8225249 : LinuxDebBundler and LinuxRpmBundler should share more code JDK-8200758-branch
authorherrick
Tue, 24 Sep 2019 13:41:16 -0400
branchJDK-8200758-branch
changeset 58301 e0efb29609bd
parent 58172 bf06a1d3aef6
child 58302 718bd56695b3
8225249 : LinuxDebBundler and LinuxRpmBundler should share more code Submitted-by: asemenyuk Reviewed-by: herrick, almatvee
make/CompileJavaModules.gmk
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.desktop
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.postinst
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.postrm
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.preinst
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.prerm
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.spec
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/utils.sh
src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/PathGroup.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/PlatformPackage.java
test/jdk/tools/jpackage/apps/image/Hello.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Test.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java
test/jdk/tools/jpackage/linux/AppCategoryTest.java
test/jdk/tools/jpackage/linux/BundleNameTest.java
test/jdk/tools/jpackage/linux/LicenseTypeTest.java
test/jdk/tools/jpackage/linux/MaintainerTest.java
test/jdk/tools/jpackage/linux/PackageDepsTest.java
test/jdk/tools/jpackage/linux/ReleaseTest.java
test/jdk/tools/jpackage/linux/ShortcutHintTest.java
test/jdk/tools/jpackage/manage_packages.sh
test/jdk/tools/jpackage/run_tests.sh
test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java
test/jdk/tools/jpackage/share/AppImagePackageTest.java
test/jdk/tools/jpackage/share/FileAssociationsTest.java
test/jdk/tools/jpackage/share/InstallDirTest.java
test/jdk/tools/jpackage/share/LicenseTest.java
test/jdk/tools/jpackage/share/RuntimePackageTest.java
test/jdk/tools/jpackage/share/SimplePackageTest.java
test/jdk/tools/jpackage/share/manage_packages.sh
test/jdk/tools/jpackage/test_jpackage.sh
test/jdk/tools/jpackage/windows/WinConsoleTest.java
test/jdk/tools/jpackage/windows/WinDirChooserTest.java
test/jdk/tools/jpackage/windows/WinMenuGroupTest.java
test/jdk/tools/jpackage/windows/WinMenuTest.java
test/jdk/tools/jpackage/windows/WinPerUserInstallTest.java
test/jdk/tools/jpackage/windows/WinShortcutTest.java
test/jdk/tools/jpackage/windows/WinUpgradeUUIDTest.java
--- a/make/CompileJavaModules.gmk	Mon Sep 16 19:24:32 2019 -0400
+++ b/make/CompileJavaModules.gmk	Tue Sep 24 13:41:16 2019 -0400
@@ -380,8 +380,8 @@
 
 ################################################################################
 
-jdk.jpackage_COPY += .gif .png .txt .spec .script .prerm .preinst .postrm .postinst .list \
-    .desktop .copyright .control .plist .template .icns .scpt .entitlements .wxs .wxl .iss .ico .bmp
+jdk.jpackage_COPY += .gif .png .txt .spec .script .prerm .preinst .postrm .postinst .list .sh \
+    .desktop .copyright .control .plist .template .icns .scpt .entitlements .wxs .wxl .ico .bmp
 
 jdk.jpackage_CLEAN += .properties
 
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java	Tue Sep 24 13:41:16 2019 -0400
@@ -110,12 +110,6 @@
         return true;
     }
 
-    // it is static for the sake of sharing with "installer" bundlers
-    // that may skip calls to validate/bundle in this class!
-    static File getRootDir(File outDir, Map<String, ? super Object> params) {
-        return new File(outDir, APP_NAME.fetchFrom(params));
-    }
-
     File doBundle(Map<String, ? super Object> params, File outputDirectory,
             boolean dependentTask) throws PackagerException {
         if (StandardBundlerParam.isRuntimeInstaller(params)) {
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java	Tue Sep 24 13:41:16 2019 -0400
@@ -46,7 +46,7 @@
         "jdk.jpackage.internal.resources.LinuxResources");
 
     private static final String LIBRARY_NAME = "libapplauncher.so";
-    private final static String DEFAULT_ICON = "java32.png";
+    final static String DEFAULT_ICON = "java32.png";
 
     private final Path root;
     private final Path appDir;
@@ -106,19 +106,7 @@
         Files.copy(in, dstFile);
     }
 
-    // it is static for the sake of sharing with "installer" bundlers
-    // that may skip calls to validate/bundle in this class!
-    public static File getRootDir(File outDir,
-            Map<String, ? super Object> params) {
-        return new File(outDir, APP_NAME.fetchFrom(params));
-    }
-
-    public static String getLauncherRelativePath(
-            Map<String, ? super Object> params) {
-        return "bin" + File.separator + APP_NAME.fetchFrom(params);
-    }
-
-    private static String getLauncherName(Map<String, ? super Object> params) {
+    public static String getLauncherName(Map<String, ? super Object> params) {
         return APP_NAME.fetchFrom(params);
     }
 
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java	Tue Sep 24 13:41:16 2019 -0400
@@ -25,15 +25,12 @@
 
 package jdk.jpackage.internal;
 
-import javax.imageio.ImageIO;
-import java.awt.image.BufferedImage;
 import java.io.*;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
-import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.BasicFileAttributes;
 
 import java.nio.file.attribute.PosixFilePermission;
@@ -44,21 +41,9 @@
 import java.util.stream.Stream;
 
 import static jdk.jpackage.internal.StandardBundlerParam.*;
-import static jdk.jpackage.internal.LinuxAppBundler.ICON_PNG;
-import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
-import static jdk.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES;
-
-public class LinuxDebBundler extends AbstractBundler {
+import static jdk.jpackage.internal.LinuxPackageBundler.I18N;
 
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-                    "jdk.jpackage.internal.resources.LinuxResources");
-
-    public static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER =
-            new StandardBundlerParam<>(
-            "linux.app.bundler",
-            LinuxAppBundler.class,
-            params -> new LinuxAppBundler(),
-            (s, p) -> null);
+public class LinuxDebBundler extends LinuxPackageBundler {
 
     // Debian rules for package naming are used here
     // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Source
@@ -68,10 +53,10 @@
     // They must be at least two characters long and
     // must start with an alphanumeric character.
     //
-    private static final Pattern DEB_BUNDLE_NAME_PATTERN =
+    private static final Pattern DEB_PACKAGE_NAME_PATTERN =
             Pattern.compile("^[a-z][a-z\\d\\+\\-\\.]+");
 
-    public static final BundlerParamInfo<String> BUNDLE_NAME =
+    private static final BundlerParamInfo<String> PACKAGE_NAME =
             new StandardBundlerParam<> (
             Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(),
             String.class,
@@ -85,7 +70,7 @@
                 return nm;
             },
             (s, p) -> {
-                if (!DEB_BUNDLE_NAME_PATTERN.matcher(s).matches()) {
+                if (!DEB_PACKAGE_NAME_PATTERN.matcher(s).matches()) {
                     throw new IllegalArgumentException(new ConfigException(
                             MessageFormat.format(I18N.getString(
                             "error.invalid-value-for-package-name"), s),
@@ -100,7 +85,7 @@
             new StandardBundlerParam<>(
                     "linux.deb.fullPackageName", String.class, params -> {
                         try {
-                            return BUNDLE_NAME.fetchFrom(params)
+                            return PACKAGE_NAME.fetchFrom(params)
                             + "_" + VERSION.fetchFrom(params)
                             + "-" + RELEASE.fetchFrom(params)
                             + "_" + getDebArch();
@@ -110,43 +95,14 @@
                         }
                     }, (s, p) -> s);
 
-    private static final BundlerParamInfo<File> DEB_IMAGE_DIR =
-            new StandardBundlerParam<>(
-            "linux.deb.imageDir",
-            File.class,
-            params -> {
-                File imagesRoot = IMAGES_ROOT.fetchFrom(params);
-                if (!imagesRoot.exists()) imagesRoot.mkdirs();
-                return new File(new File(imagesRoot, "linux-deb.image"),
-                        FULL_PACKAGE_NAME.fetchFrom(params));
-            },
-            (s, p) -> new File(s));
-
-    public static final BundlerParamInfo<File> APP_IMAGE_ROOT =
-            new StandardBundlerParam<>(
-            "linux.deb.imageRoot",
-            File.class,
-            params -> {
-                File imageDir = DEB_IMAGE_DIR.fetchFrom(params);
-                return new File(imageDir, LINUX_INSTALL_DIR.fetchFrom(params));
-            },
-            (s, p) -> new File(s));
-
-    public static final BundlerParamInfo<File> CONFIG_DIR =
-            new StandardBundlerParam<>(
-            "linux.deb.configDir",
-            File.class,
-            params ->  new File(DEB_IMAGE_DIR.fetchFrom(params), "DEBIAN"),
-            (s, p) -> new File(s));
-
-    public static final BundlerParamInfo<String> EMAIL =
+    private static final BundlerParamInfo<String> EMAIL =
             new StandardBundlerParam<> (
             Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId(),
             String.class,
             params -> "Unknown",
             (s, p) -> s);
 
-    public static final BundlerParamInfo<String> MAINTAINER =
+    private static final BundlerParamInfo<String> MAINTAINER =
             new StandardBundlerParam<> (
             BundleParams.PARAM_MAINTAINER,
             String.class,
@@ -154,14 +110,14 @@
                     + EMAIL.fetchFrom(params) + ">",
             (s, p) -> s);
 
-    public static final BundlerParamInfo<String> SECTION =
+    private static final BundlerParamInfo<String> SECTION =
             new StandardBundlerParam<>(
             Arguments.CLIOptions.LINUX_CATEGORY.getId(),
             String.class,
             params -> "misc",
             (s, p) -> s);
 
-    public static final BundlerParamInfo<String> LICENSE_TEXT =
+    private static final BundlerParamInfo<String> LICENSE_TEXT =
             new StandardBundlerParam<> (
             "linux.deb.licenseText",
             String.class,
@@ -184,65 +140,13 @@
             },
             (s, p) -> s);
 
-    public static final BundlerParamInfo<String> COPYRIGHT_FILE =
+    private static final BundlerParamInfo<String> COPYRIGHT_FILE =
             new StandardBundlerParam<>(
             Arguments.CLIOptions.LINUX_DEB_COPYRIGHT_FILE.getId(),
             String.class,
             params -> null,
             (s, p) -> s);
 
-    public static final BundlerParamInfo<String> XDG_FILE_PREFIX =
-            new StandardBundlerParam<> (
-            "linux.xdg-prefix",
-            String.class,
-            params -> {
-                try {
-                    String vendor;
-                    if (params.containsKey(VENDOR.getID())) {
-                        vendor = VENDOR.fetchFrom(params);
-                    } else {
-                        vendor = "jpackage";
-                    }
-                    String appName = APP_NAME.fetchFrom(params);
-
-                    return (appName + "-" + vendor).replaceAll("\\s", "");
-                } catch (Exception e) {
-                    Log.verbose(e);
-                }
-                return "unknown-MimeInfo.xml";
-            },
-            (s, p) -> s);
-
-    public static final BundlerParamInfo<String> MENU_GROUP =
-        new StandardBundlerParam<>(
-                Arguments.CLIOptions.LINUX_MENU_GROUP.getId(),
-                String.class,
-                params -> I18N.getString("param.menu-group.default"),
-                (s, p) -> s
-        );
-
-    public static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
-        new StandardBundlerParam<>(
-                Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId(),
-                Boolean.class,
-                params -> false,
-                (s, p) -> (s == null || "null".equalsIgnoreCase(s))
-                        ? false : Boolean.valueOf(s)
-        );
-
-    private final static String DEFAULT_ICON = "java32.png";
-    private final static String DEFAULT_CONTROL_TEMPLATE = "template.control";
-    private final static String DEFAULT_PRERM_TEMPLATE = "template.prerm";
-    private final static String DEFAULT_PREINSTALL_TEMPLATE =
-            "template.preinst";
-    private final static String DEFAULT_POSTRM_TEMPLATE = "template.postrm";
-    private final static String DEFAULT_POSTINSTALL_TEMPLATE =
-            "template.postinst";
-    private final static String DEFAULT_COPYRIGHT_TEMPLATE =
-            "template.copyright";
-    private final static String DEFAULT_DESKTOP_FILE_TEMPLATE =
-            "template.desktop";
-
     private final static String TOOL_DPKG_DEB = "dpkg-deb";
     private final static String TOOL_DPKG = "dpkg";
 
@@ -261,131 +165,43 @@
         return true;
     }
 
-    @Override
-    public boolean validate(Map<String, ? super Object> params)
-            throws ConfigException {
-        try {
-            if (params == null) throw new ConfigException(
-                    I18N.getString("error.parameters-null"),
-                    I18N.getString("error.parameters-null.advice"));
-
-            //run basic validation to ensure requirements are met
-            //we are not interested in return code, only possible exception
-            APP_BUNDLER.fetchFrom(params).validate(params);
+    public LinuxDebBundler() {
+        super(PACKAGE_NAME);
+    }
 
-            // NOTE: Can we validate that the required tools are available
-            // before we start?
-            if (!testTool(TOOL_DPKG_DEB, "1")){
-                throw new ConfigException(MessageFormat.format(
-                        I18N.getString("error.tool-not-found"), TOOL_DPKG_DEB),
-                        I18N.getString("error.tool-not-found.advice"));
-            }
-            if (!testTool(TOOL_DPKG, "1")){
-                throw new ConfigException(MessageFormat.format(
-                        I18N.getString("error.tool-not-found"), TOOL_DPKG),
-                        I18N.getString("error.tool-not-found.advice"));
-            }
+    @Override
+    public void doValidate(Map<String, ? super Object> params)
+            throws ConfigException {
+        // NOTE: Can we validate that the required tools are available
+        // before we start?
+        if (!testTool(TOOL_DPKG_DEB, "1")){
+            throw new ConfigException(MessageFormat.format(
+                    I18N.getString("error.tool-not-found"), TOOL_DPKG_DEB),
+                    I18N.getString("error.tool-not-found.advice"));
+        }
+        if (!testTool(TOOL_DPKG, "1")){
+            throw new ConfigException(MessageFormat.format(
+                    I18N.getString("error.tool-not-found"), TOOL_DPKG),
+                    I18N.getString("error.tool-not-found.advice"));
+        }
 
 
-            // Show warning is license file is missing
-            String licenseFile = LICENSE_FILE.fetchFrom(params);
-            if (licenseFile == null) {
-                Log.verbose(I18N.getString("message.debs-like-licenses"));
-            }
-
-            // only one mime type per association, at least one file extention
-            List<Map<String, ? super Object>> associations =
-                    FILE_ASSOCIATIONS.fetchFrom(params);
-            if (associations != null) {
-                for (int i = 0; i < associations.size(); i++) {
-                    Map<String, ? super Object> assoc = associations.get(i);
-                    List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
-                    if (mimes == null || mimes.isEmpty()) {
-                        String msgKey =
-                            "error.no-content-types-for-file-association";
-                        throw new ConfigException(
-                                MessageFormat.format(I18N.getString(msgKey), i),
-                                I18N.getString(msgKey + ".advise"));
-
-                    } else if (mimes.size() > 1) {
-                        String msgKey =
-                            "error.too-many-content-types-for-file-association";
-                        throw new ConfigException(
-                                MessageFormat.format(I18N.getString(msgKey), i),
-                                I18N.getString(msgKey + ".advise"));
-                    }
-                }
-            }
-
-            // bundle name has some restrictions
-            // the string converter will throw an exception if invalid
-            BUNDLE_NAME.getStringConverter().apply(
-                    BUNDLE_NAME.fetchFrom(params), params);
-
-            return true;
-        } catch (RuntimeException re) {
-            if (re.getCause() instanceof ConfigException) {
-                throw (ConfigException) re.getCause();
-            } else {
-                throw new ConfigException(re);
-            }
+        // Show warning is license file is missing
+        String licenseFile = LICENSE_FILE.fetchFrom(params);
+        if (licenseFile == null) {
+            Log.verbose(I18N.getString("message.debs-like-licenses"));
         }
     }
 
-    private boolean prepareProto(Map<String, ? super Object> params)
-            throws PackagerException, IOException {
-        File appImage = StandardBundlerParam.getPredefinedAppImage(params);
-
-        // we either have an application image or need to build one
-        if (appImage != null) {
-            // copy everything from appImage dir into appDir/name
-            IOUtils.copyRecursive(appImage.toPath(),
-                    getConfig_RootDirectory(params).toPath());
-        } else {
-            File bundleDir = APP_BUNDLER.fetchFrom(params).doBundle(params,
-                    APP_IMAGE_ROOT.fetchFrom(params), true);
-            if (bundleDir == null) {
-                return false;
-            }
-            Files.move(bundleDir.toPath(), getConfig_RootDirectory(
-                    params).toPath(), StandardCopyOption.REPLACE_EXISTING);
-        }
-        return true;
-    }
-
-    public File bundle(Map<String, ? super Object> params,
-            File outdir) throws PackagerException {
-
-        IOUtils.writableOutputDir(outdir.toPath());
+    @Override
+    protected File buildPackageBundle(
+            Map<String, String> replacementData,
+            Map<String, ? super Object> params, File outputParentDir) throws
+            PackagerException, IOException {
 
-        // we want to create following structure
-        //   <package-name>
-        //        DEBIAN
-        //          control   (file with main package details)
-        //          menu      (request to create menu)
-        //          ... other control files if needed ....
-        //        opt  (by default)
-        //          AppFolder (this is where app image goes)
-        //             launcher executable
-        //             app
-        //             runtime
-
-        File imageDir = DEB_IMAGE_DIR.fetchFrom(params);
-        File configDir = CONFIG_DIR.fetchFrom(params);
-
-        try {
-
-            imageDir.mkdirs();
-            configDir.mkdirs();
-            if (prepareProto(params) && prepareProjectConfig(params)) {
-                adjustPermissionsRecursive(imageDir);
-                return buildDeb(params, outdir);
-            }
-            return null;
-        } catch (IOException ex) {
-            Log.verbose(ex);
-            throw new PackagerException(ex);
-        }
+        prepareProjectConfig(replacementData, params);
+        adjustPermissionsRecursive(createMetaPackage(params).sourceRoot().toFile());
+        return buildDeb(params, outputParentDir);
     }
 
     /*
@@ -429,26 +245,6 @@
         return false;
     }
 
-    private long getInstalledSizeKB(Map<String, ? super Object> params) {
-        return getInstalledSizeKB(APP_IMAGE_ROOT.fetchFrom(params)) >> 10;
-    }
-
-    private long getInstalledSizeKB(File dir) {
-        long count = 0;
-        File[] children = dir.listFiles();
-        if (children != null) {
-            for (File file : children) {
-                if (file.isFile()) {
-                    count += file.length();
-                }
-                else if (file.isDirectory()) {
-                    count += getInstalledSizeKB(file);
-                }
-            }
-        }
-        return count;
-    }
-
     private void adjustPermissionsRecursive(File dir) throws IOException {
         Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<Path>() {
             @Override
@@ -477,327 +273,61 @@
         });
     }
 
-    private boolean prepareProjectConfig(Map<String, ? super Object> params)
-            throws IOException {
-        Map<String, String> data = createReplacementData(params);
-        File rootDir = getConfig_RootDirectory(params);
-        File binDir = new File(rootDir, "bin");
+    private class DebianFile {
+
+        DebianFile(Path dstFilePath, String comment) {
+            this.dstFilePath = dstFilePath;
+            this.comment = comment;
+        }
 
-        File iconTarget = getConfig_IconFile(binDir, params);
-        File icon = ICON_PNG.fetchFrom(params);
-        if (!StandardBundlerParam.isRuntimeInstaller(params)) {
-            // prepare installer icon
-            if (icon == null || !icon.exists()) {
-                fetchResource(iconTarget.getName(),
-                        I18N.getString("resource.menu-icon"),
-                        DEFAULT_ICON,
-                        iconTarget,
+        DebianFile setExecutable() {
+            permissions = "rwxr-xr-x";
+            return this;
+        }
+
+        void create(Map<String, String> data, Map<String, ? super Object> params)
+                throws IOException {
+            Files.createDirectories(dstFilePath.getParent());
+            try (Writer w = Files.newBufferedWriter(dstFilePath)) {
+                String content = preprocessTextResource(
+                        dstFilePath.getFileName().toString(),
+                        I18N.getString(comment),
+                        "template." + dstFilePath.getFileName().toString(),
+                        data,
                         VERBOSE.fetchFrom(params),
                         RESOURCE_DIR.fetchFrom(params));
-            } else {
-                fetchResource(iconTarget.getName(),
-                        I18N.getString("resource.menu-icon"),
-                        icon,
-                        iconTarget,
-                        VERBOSE.fetchFrom(params),
-                        RESOURCE_DIR.fetchFrom(params));
+                w.write(content);
+            }
+            if (permissions != null) {
+                setPermissions(dstFilePath.toFile(), permissions);
             }
         }
 
-        StringBuilder installScripts = new StringBuilder();
-        StringBuilder removeScripts = new StringBuilder();
-        for (Map<String, ? super Object> addLauncher :
-                ADD_LAUNCHERS.fetchFrom(params)) {
-            Map<String, String> addLauncherData =
-                    createReplacementData(addLauncher);
-            addLauncherData.put("APPLICATION_FS_NAME",
-                    data.get("APPLICATION_FS_NAME"));
-            addLauncherData.put("DESKTOP_MIMES", "");
-
-            if (!StandardBundlerParam.isRuntimeInstaller(params)) {
-                // prepare desktop shortcut
-                if (SHORTCUT_HINT.fetchFrom(params)) {
-                    try (Writer w = Files.newBufferedWriter(
-                        getConfig_DesktopShortcutFile(
-                                binDir, addLauncher).toPath())) {
-                        String content = preprocessTextResource(
-                            getConfig_DesktopShortcutFile(binDir,
-                            addLauncher).getName(),
-                            I18N.getString("resource.menu-shortcut-descriptor"),
-                            DEFAULT_DESKTOP_FILE_TEMPLATE,
-                            addLauncherData,
-                            VERBOSE.fetchFrom(params),
-                            RESOURCE_DIR.fetchFrom(params));
-                        w.write(content);
-                    }
-                }
-            }
-
-            // prepare installer icon
-            iconTarget = getConfig_IconFile(binDir, addLauncher);
-            icon = ICON_PNG.fetchFrom(addLauncher);
-            if (icon == null || !icon.exists()) {
-                fetchResource(iconTarget.getName(),
-                        I18N.getString("resource.menu-icon"),
-                        DEFAULT_ICON,
-                        iconTarget,
-                        VERBOSE.fetchFrom(params),
-                        RESOURCE_DIR.fetchFrom(params));
-            } else {
-                fetchResource(iconTarget.getName(),
-                        I18N.getString("resource.menu-icon"),
-                        icon,
-                        iconTarget,
-                        VERBOSE.fetchFrom(params),
-                        RESOURCE_DIR.fetchFrom(params));
-            }
-
-            // postinst copying of desktop icon
-            installScripts.append(
-                    "        xdg-desktop-menu install --novendor ");
-            installScripts.append(LINUX_INSTALL_DIR.fetchFrom(params));
-            installScripts.append("/");
-            installScripts.append(data.get("APPLICATION_FS_NAME"));
-            installScripts.append("/bin/");
-            installScripts.append(
-                    addLauncherData.get("APPLICATION_LAUNCHER_FILENAME"));
-            installScripts.append(".desktop\n");
+        private final Path dstFilePath;
+        private final String comment;
+        private String permissions;
+    }
 
-            // postrm cleanup of desktop icon
-            removeScripts.append(
-                    "        xdg-desktop-menu uninstall --novendor ");
-            removeScripts.append(LINUX_INSTALL_DIR.fetchFrom(params));
-            removeScripts.append("/");
-            removeScripts.append(data.get("APPLICATION_FS_NAME"));
-            removeScripts.append("/bin/");
-            removeScripts.append(
-                    addLauncherData.get("APPLICATION_LAUNCHER_FILENAME"));
-            removeScripts.append(".desktop\n");
-        }
-        data.put("ADD_LAUNCHERS_INSTALL", installScripts.toString());
-        data.put("ADD_LAUNCHERS_REMOVE", removeScripts.toString());
-
-        List<Map<String, ? super Object>> associations =
-                FILE_ASSOCIATIONS.fetchFrom(params);
-        data.put("FILE_ASSOCIATION_INSTALL", "");
-        data.put("FILE_ASSOCIATION_REMOVE", "");
-        data.put("DESKTOP_MIMES", "");
-        if (associations != null) {
-            String mimeInfoFile = XDG_FILE_PREFIX.fetchFrom(params)
-                    + "-MimeInfo.xml";
-            StringBuilder mimeInfo = new StringBuilder(
-                "<?xml version=\"1.0\"?>\n<mime-info xmlns="
-                + "'http://www.freedesktop.org/standards/shared-mime-info'>\n");
-            StringBuilder registrations = new StringBuilder();
-            StringBuilder deregistrations = new StringBuilder();
-            StringBuilder desktopMimes = new StringBuilder("MimeType=");
-            boolean addedEntry = false;
-
-            for (Map<String, ? super Object> assoc : associations) {
-                //  <mime-type type="application/x-vnd.awesome">
-                //    <comment>Awesome document</comment>
-                //    <glob pattern="*.awesome"/>
-                //    <glob pattern="*.awe"/>
-                //  </mime-type>
-
-                if (assoc == null) {
-                    continue;
-                }
-
-                String description = FA_DESCRIPTION.fetchFrom(assoc);
-                File faIcon = FA_ICON.fetchFrom(assoc);
-                List<String> extensions = FA_EXTENSIONS.fetchFrom(assoc);
-                if (extensions == null) {
-                    Log.error(I18N.getString(
-                          "message.creating-association-with-null-extension"));
-                }
-
-                List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
-                if (mimes == null || mimes.isEmpty()) {
-                    continue;
-                }
-                String thisMime = mimes.get(0);
-                String dashMime = thisMime.replace('/', '-');
-
-                mimeInfo.append("  <mime-type type='")
-                        .append(thisMime)
-                        .append("'>\n");
-                if (description != null && !description.isEmpty()) {
-                    mimeInfo.append("    <comment>")
-                            .append(description)
-                            .append("</comment>\n");
-                }
-
-                if (extensions != null) {
-                    for (String ext : extensions) {
-                        mimeInfo.append("    <glob pattern='*.")
-                                .append(ext)
-                                .append("'/>\n");
-                    }
-                }
-
-                mimeInfo.append("  </mime-type>\n");
-                if (!addedEntry) {
-                    registrations.append("        xdg-mime install ")
-                            .append(LINUX_INSTALL_DIR.fetchFrom(params))
-                            .append("/")
-                            .append(data.get("APPLICATION_FS_NAME"))
-                            .append("/bin/")
-                            .append(mimeInfoFile)
-                            .append("\n");
+    private void prepareProjectConfig(Map<String, String> data,
+            Map<String, ? super Object> params) throws IOException {
 
-                    deregistrations.append("        xdg-mime uninstall ")
-                            .append(LINUX_INSTALL_DIR.fetchFrom(params))
-                            .append("/")
-                            .append(data.get("APPLICATION_FS_NAME"))
-                            .append("/bin/")
-                            .append(mimeInfoFile)
-                            .append("\n");
-                    addedEntry = true;
-                } else {
-                    desktopMimes.append(";");
-                }
-                desktopMimes.append(thisMime);
-
-                if (faIcon != null && faIcon.exists()) {
-                    int size = getSquareSizeOfImage(faIcon);
-
-                    if (size > 0) {
-                        File target = new File(binDir,
-                                APP_NAME.fetchFrom(params)
-                                + "_fa_" + faIcon.getName());
-                        IOUtils.copyFile(faIcon, target);
-
-                        // xdg-icon-resource install --context mimetypes
-                        // --size 64 awesomeapp_fa_1.png
-                        // application-x.vnd-awesome
-                        registrations.append(
-                                "        xdg-icon-resource install "
-                                        + "--context mimetypes --size ")
-                                .append(size)
-                                .append(" ")
-                                .append(LINUX_INSTALL_DIR.fetchFrom(params))
-                                .append("/")
-                                .append(data.get("APPLICATION_FS_NAME"))
-                                .append("/")
-                                .append(target.getName())
-                                .append(" ")
-                                .append(dashMime)
-                                .append("\n");
-
-                        // x dg-icon-resource uninstall --context mimetypes
-                        // --size 64 awesomeapp_fa_1.png
-                        // application-x.vnd-awesome
-                        deregistrations.append(
-                                "        xdg-icon-resource uninstall "
-                                        + "--context mimetypes --size ")
-                                .append(size)
-                                .append(" ")
-                                .append(LINUX_INSTALL_DIR.fetchFrom(params))
-                                .append("/")
-                                .append(data.get("APPLICATION_FS_NAME"))
-                                .append("/")
-                                .append(target.getName())
-                                .append(" ")
-                                .append(dashMime)
-                                .append("\n");
-                    }
-                }
-            }
-            mimeInfo.append("</mime-info>");
-
-            if (addedEntry) {
-                try (Writer w = Files.newBufferedWriter(
-                        new File(binDir, mimeInfoFile).toPath())) {
-                    w.write(mimeInfo.toString());
-                }
-                data.put("FILE_ASSOCIATION_INSTALL", registrations.toString());
-                data.put("FILE_ASSOCIATION_REMOVE", deregistrations.toString());
-                data.put("DESKTOP_MIMES", desktopMimes.toString());
-            }
-        }
-
-        if (!StandardBundlerParam.isRuntimeInstaller(params)) {
-            // prepare desktop shortcut
-            if (SHORTCUT_HINT.fetchFrom(params)) {
-                try (Writer w = Files.newBufferedWriter(
-                    getConfig_DesktopShortcutFile(binDir, params).toPath())) {
-                    String content = preprocessTextResource(
-                        getConfig_DesktopShortcutFile(
-                        binDir, params).getName(),
-                        I18N.getString("resource.menu-shortcut-descriptor"),
-                        DEFAULT_DESKTOP_FILE_TEMPLATE,
-                        data,
-                        VERBOSE.fetchFrom(params),
-                        RESOURCE_DIR.fetchFrom(params));
-                    w.write(content);
-                }
-            }
-        }
-        // prepare control file
-        try (Writer w = Files.newBufferedWriter(
-                getConfig_ControlFile(params).toPath())) {
-            String content = preprocessTextResource(
-                    getConfig_ControlFile(params).getName(),
-                    I18N.getString("resource.deb-control-file"),
-                    DEFAULT_CONTROL_TEMPLATE,
-                    data,
-                    VERBOSE.fetchFrom(params),
-                    RESOURCE_DIR.fetchFrom(params));
-            w.write(content);
-        }
-
-        try (Writer w = Files.newBufferedWriter(
-                getConfig_PreinstallFile(params).toPath())) {
-            String content = preprocessTextResource(
-                    getConfig_PreinstallFile(params).getName(),
-                    I18N.getString("resource.deb-preinstall-script"),
-                    DEFAULT_PREINSTALL_TEMPLATE,
-                    data,
-                    VERBOSE.fetchFrom(params),
-                    RESOURCE_DIR.fetchFrom(params));
-            w.write(content);
-        }
-        setPermissions(getConfig_PreinstallFile(params), "rwxr-xr-x");
-
-        try (Writer w = Files.newBufferedWriter(
-                    getConfig_PrermFile(params).toPath())) {
-            String content = preprocessTextResource(
-                    getConfig_PrermFile(params).getName(),
-                    I18N.getString("resource.deb-prerm-script"),
-                    DEFAULT_PRERM_TEMPLATE,
-                    data,
-                    VERBOSE.fetchFrom(params),
-                    RESOURCE_DIR.fetchFrom(params));
-            w.write(content);
-        }
-        setPermissions(getConfig_PrermFile(params), "rwxr-xr-x");
-
-        try (Writer w = Files.newBufferedWriter(
-                getConfig_PostinstallFile(params).toPath())) {
-            String content = preprocessTextResource(
-                    getConfig_PostinstallFile(params).getName(),
-                    I18N.getString("resource.deb-postinstall-script"),
-                    DEFAULT_POSTINSTALL_TEMPLATE,
-                    data,
-                    VERBOSE.fetchFrom(params),
-                    RESOURCE_DIR.fetchFrom(params));
-            w.write(content);
-        }
-        setPermissions(getConfig_PostinstallFile(params), "rwxr-xr-x");
-
-        try (Writer w = Files.newBufferedWriter(
-                getConfig_PostrmFile(params).toPath())) {
-            String content = preprocessTextResource(
-                    getConfig_PostrmFile(params).getName(),
-                    I18N.getString("resource.deb-postrm-script"),
-                    DEFAULT_POSTRM_TEMPLATE,
-                    data,
-                    VERBOSE.fetchFrom(params),
-                    RESOURCE_DIR.fetchFrom(params));
-            w.write(content);
-        }
-        setPermissions(getConfig_PostrmFile(params), "rwxr-xr-x");
+        Path configDir = createMetaPackage(params).sourceRoot().resolve("DEBIAN");
+        List<DebianFile> debianFiles = new ArrayList<>();
+        debianFiles.add(new DebianFile(
+                configDir.resolve("control"),
+                "resource.deb-control-file"));
+        debianFiles.add(new DebianFile(
+                configDir.resolve("preinst"),
+                "resource.deb-preinstall-script").setExecutable());
+        debianFiles.add(new DebianFile(
+                configDir.resolve("prerm"),
+                "resource.deb-prerm-script").setExecutable());
+        debianFiles.add(new DebianFile(
+                configDir.resolve("postinst"),
+                "resource.deb-postinstall-script").setExecutable());
+        debianFiles.add(new DebianFile(
+                configDir.resolve("postrm"),
+                "resource.deb-postrm-script").setExecutable());
 
         getConfig_CopyrightFile(params).getParentFile().mkdirs();
         String customCopyrightFile = COPYRIGHT_FILE.fetchFrom(params);
@@ -805,93 +335,36 @@
             IOUtils.copyFile(new File(customCopyrightFile),
                     getConfig_CopyrightFile(params));
         } else {
-            try (Writer w = Files.newBufferedWriter(
-                    getConfig_CopyrightFile(params).toPath())) {
-                String content = preprocessTextResource(
-                        getConfig_CopyrightFile(params).getName(),
-                        I18N.getString("resource.copyright-file"),
-                        DEFAULT_COPYRIGHT_TEMPLATE,
-                        data,
-                        VERBOSE.fetchFrom(params),
-                        RESOURCE_DIR.fetchFrom(params));
-                w.write(content);
-            }
+            debianFiles.add(new DebianFile(
+                    getConfig_CopyrightFile(params).toPath(),
+                    "resource.copyright-file"));
         }
 
-        return true;
+        for (DebianFile debianFile : debianFiles) {
+            debianFile.create(data, params);
+        }
     }
 
-    private Map<String, String> createReplacementData(
+    @Override
+    protected Map<String, String> createReplacementData(
             Map<String, ? super Object> params) throws IOException {
         Map<String, String> data = new HashMap<>();
-        String launcher = LinuxAppImageBuilder.getLauncherRelativePath(params);
 
-        data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params));
-        data.put("APPLICATION_FS_NAME",
-                getConfig_RootDirectory(params).getName());
-        data.put("APPLICATION_PACKAGE", BUNDLE_NAME.fetchFrom(params));
-        data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
         data.put("APPLICATION_MAINTAINER", MAINTAINER.fetchFrom(params));
-        data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
-        data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params));
         data.put("APPLICATION_SECTION", SECTION.fetchFrom(params));
-        data.put("APPLICATION_LAUNCHER_FILENAME", launcher);
-        data.put("INSTALLATION_DIRECTORY", LINUX_INSTALL_DIR.fetchFrom(params));
-        data.put("XDG_PREFIX", XDG_FILE_PREFIX.fetchFrom(params));
-        data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params));
-        data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
         data.put("APPLICATION_COPYRIGHT", COPYRIGHT.fetchFrom(params));
         data.put("APPLICATION_LICENSE_TEXT", LICENSE_TEXT.fetchFrom(params));
         data.put("APPLICATION_ARCH", getDebArch());
-        data.put("APPLICATION_INSTALLED_SIZE",
-                Long.toString(getInstalledSizeKB(params)));
-        data.put("PACKAGE_DEPENDENCIES", LINUX_PACKAGE_DEPENDENCIES.fetchFrom(
-                params));
-        data.put("RUNTIME_INSTALLER", "" +
-                StandardBundlerParam.isRuntimeInstaller(params));
+        data.put("APPLICATION_INSTALLED_SIZE", Long.toString(
+                createMetaPackage(params).sourceApplicationLayout().sizeInBytes() >> 10));
 
         return data;
     }
 
-    private File getConfig_DesktopShortcutFile(File rootDir,
-            Map<String, ? super Object> params) {
-        return new File(rootDir, APP_NAME.fetchFrom(params) + ".desktop");
-    }
-
-    private File getConfig_IconFile(File rootDir,
-            Map<String, ? super Object> params) {
-        return new File(rootDir, APP_NAME.fetchFrom(params) + ".png");
-    }
-
-    private File getConfig_ControlFile(Map<String, ? super Object> params) {
-        return new File(CONFIG_DIR.fetchFrom(params), "control");
-    }
-
-    private File getConfig_PreinstallFile(Map<String, ? super Object> params) {
-        return new File(CONFIG_DIR.fetchFrom(params), "preinst");
-    }
-
-    private File getConfig_PrermFile(Map<String, ? super Object> params) {
-        return new File(CONFIG_DIR.fetchFrom(params), "prerm");
-    }
-
-    private File getConfig_PostinstallFile(Map<String, ? super Object> params) {
-        return new File(CONFIG_DIR.fetchFrom(params), "postinst");
-    }
-
-    private File getConfig_PostrmFile(Map<String, ? super Object> params) {
-        return new File(CONFIG_DIR.fetchFrom(params), "postrm");
-    }
-
     private File getConfig_CopyrightFile(Map<String, ? super Object> params) {
-        return Path.of(DEB_IMAGE_DIR.fetchFrom(params).getAbsolutePath(), "usr",
-                "share", "doc", BUNDLE_NAME.fetchFrom(params), "copyright").toFile();
-    }
-
-    private File getConfig_RootDirectory(
-            Map<String, ? super Object> params) {
-        return Path.of(APP_IMAGE_ROOT.fetchFrom(params).getAbsolutePath(),
-                BUNDLE_NAME.fetchFrom(params)).toFile();
+        PlatformPackage thePackage = createMetaPackage(params);
+        return thePackage.sourceRoot().resolve(Path.of("usr/share/doc",
+                thePackage.name(), "copyright")).toFile();
     }
 
     private File buildDeb(Map<String, ? super Object> params,
@@ -901,14 +374,13 @@
         Log.verbose(MessageFormat.format(I18N.getString(
                 "message.outputting-to-location"), outFile.getAbsolutePath()));
 
-        outFile.getParentFile().mkdirs();
+        PlatformPackage thePackage = createMetaPackage(params);
 
         // run dpkg
         ProcessBuilder pb = new ProcessBuilder(
                 "fakeroot", TOOL_DPKG_DEB, "-b",
-                FULL_PACKAGE_NAME.fetchFrom(params),
+                thePackage.sourceRoot().toString(),
                 outFile.getAbsolutePath());
-        pb = pb.directory(DEB_IMAGE_DIR.fetchFrom(params).getParentFile());
         IOUtils.exec(pb);
 
         Log.verbose(MessageFormat.format(I18N.getString(
@@ -928,22 +400,7 @@
     }
 
     @Override
-    public String getBundleType() {
-        return "INSTALLER";
-    }
-
-    @Override
-    public File execute(Map<String, ? super Object> params,
-            File outputParentDir) throws PackagerException {
-        return bundle(params, outputParentDir);
-    }
-
-    @Override
     public boolean supported(boolean runtimeInstaller) {
-        return isSupported();
-    }
-
-    public static boolean isSupported() {
         if (Platform.getPlatform() == Platform.LINUX) {
             if (testTool(TOOL_DPKG_DEB, "1")) {
                 return true;
@@ -952,20 +409,6 @@
         return false;
     }
 
-    public int getSquareSizeOfImage(File f) {
-        try {
-            BufferedImage bi = ImageIO.read(f);
-            if (bi.getWidth() == bi.getHeight()) {
-                return bi.getWidth();
-            } else {
-                return 0;
-            }
-        } catch (Exception e) {
-            Log.verbose(e);
-            return 0;
-        }
-    }
-
     @Override
     public boolean isDefault() {
         return isDebian();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,712 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.jpackage.internal;
+
+import java.awt.image.BufferedImage;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.imageio.ImageIO;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
+import static jdk.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES;
+import static jdk.jpackage.internal.LinuxAppImageBuilder.DEFAULT_ICON;
+import static jdk.jpackage.internal.LinuxAppImageBuilder.ICON_PNG;
+import static jdk.jpackage.internal.StandardBundlerParam.*;
+
+
+abstract class LinuxPackageBundler extends AbstractBundler {
+
+    protected static final ResourceBundle I18N = ResourceBundle.getBundle(
+            "jdk.jpackage.internal.resources.LinuxResources");
+
+    private static final String DESKTOP_COMMANDS_INSTALL = "DESKTOP_COMMANDS_INSTALL";
+    private static final String DESKTOP_COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL";
+    private static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS";
+
+    private static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER =
+        new StandardBundlerParam<>(
+                "linux.app.bundler",
+                LinuxAppBundler.class,
+                (params) -> new LinuxAppBundler(),
+                null
+        );
+
+    private static final BundlerParamInfo<String> MENU_GROUP =
+        new StandardBundlerParam<>(
+                Arguments.CLIOptions.LINUX_MENU_GROUP.getId(),
+                String.class,
+                params -> I18N.getString("param.menu-group.default"),
+                (s, p) -> s
+        );
+
+    private static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
+        new StandardBundlerParam<>(
+                Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId(),
+                Boolean.class,
+                params -> false,
+                (s, p) -> (s == null || "null".equalsIgnoreCase(s))
+                        ? false : Boolean.valueOf(s)
+        );
+
+    LinuxPackageBundler(BundlerParamInfo<String> packageName) {
+        this.packageName = packageName;
+    }
+
+    private final BundlerParamInfo<String> packageName;
+
+    @Override
+    final public boolean validate(Map<String, ? super Object> params)
+            throws ConfigException {
+        try {
+            if (params == null) throw new ConfigException(
+                    I18N.getString("error.parameters-null"),
+                    I18N.getString("error.parameters-null.advice"));
+
+            // run basic validation to ensure requirements are met
+            // we are not interested in return code, only possible exception
+            APP_BUNDLER.fetchFrom(params).validate(params);
+
+            validateFileAssociations(FILE_ASSOCIATIONS.fetchFrom(params));
+
+            // If package name has some restrictions, the string converter will
+            // throw an exception if invalid
+            packageName.getStringConverter().apply(packageName.fetchFrom(params),
+                params);
+
+            // Packaging specific validation
+            doValidate(params);
+
+            return true;
+        } catch (RuntimeException re) {
+            if (re.getCause() instanceof ConfigException) {
+                throw (ConfigException) re.getCause();
+            } else {
+                throw new ConfigException(re);
+            }
+        }
+    }
+
+    @Override
+    final public String getBundleType() {
+        return "INSTALLER";
+    }
+
+    @Override
+    final public File execute(Map<String, ? super Object> params,
+            File outputParentDir) throws PackagerException {
+        IOUtils.writableOutputDir(outputParentDir.toPath());
+
+        PlatformPackage thePackage = createMetaPackage(params);
+
+        try {
+            File appImage = StandardBundlerParam.getPredefinedAppImage(params);
+
+            // we either have an application image or need to build one
+            if (appImage != null) {
+                appImageLayout(params).resolveAt(appImage.toPath()).copy(
+                        thePackage.sourceApplicationLayout());
+            } else {
+                appImage = APP_BUNDLER.fetchFrom(params).doBundle(params,
+                        thePackage.sourceRoot().toFile(), true);
+                ApplicationLayout srcAppLayout = appImageLayout(params).resolveAt(
+                        appImage.toPath());
+                if (appImage.equals(PREDEFINED_RUNTIME_IMAGE.fetchFrom(params))) {
+                    // Application image points to run-time image.
+                    // Copy it.
+                    srcAppLayout.copy(thePackage.sourceApplicationLayout());
+                } else {
+                    // Application image is a newly created directory tree.
+                    // Move it.
+                    srcAppLayout.move(thePackage.sourceApplicationLayout());
+                    if (appImage.exists()) {
+                        // Empty app image directory might remain after all application
+                        // directories have been moved.
+                        appImage.delete();
+                    }
+                }
+            }
+
+            Map<String, String> data = createDefaultReplacementData(params);
+            if (StandardBundlerParam.isRuntimeInstaller(params)) {
+                Stream.of(DESKTOP_COMMANDS_INSTALL, DESKTOP_COMMANDS_UNINSTALL,
+                        UTILITY_SCRIPTS).forEach(v -> data.put(v, ""));
+            } else {
+                data.putAll(
+                        new DesktopIntegration(thePackage, params).prepareForApplication());
+            }
+
+            data.putAll(createReplacementData(params));
+
+            return buildPackageBundle(Collections.unmodifiableMap(data), params,
+                    outputParentDir);
+        } catch (IOException ex) {
+            Log.verbose(ex);
+            throw new PackagerException(ex);
+        }
+    }
+
+    private Map<String, String> createDefaultReplacementData(
+            Map<String, ? super Object> params) throws IOException {
+        Map<String, String> data = new HashMap<>();
+
+        data.put("APPLICATION_PACKAGE", createMetaPackage(params).name());
+        data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
+        data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
+        data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
+        data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params));
+        data.put("PACKAGE_DEPENDENCIES", LINUX_PACKAGE_DEPENDENCIES.fetchFrom(
+                params));
+
+        return data;
+    }
+
+    abstract void doValidate(Map<String, ? super Object> params)
+            throws ConfigException;
+
+    abstract protected Map<String, String> createReplacementData(
+            Map<String, ? super Object> params) throws IOException;
+
+    abstract protected File buildPackageBundle(
+            Map<String, String> replacementData,
+            Map<String, ? super Object> params, File outputParentDir) throws
+            PackagerException, IOException;
+
+    final protected PlatformPackage createMetaPackage(
+            Map<String, ? super Object> params) {
+        return new PlatformPackage() {
+            @Override
+            public String name() {
+                return packageName.fetchFrom(params);
+            }
+
+            @Override
+            public Path sourceRoot() {
+                return IMAGES_ROOT.fetchFrom(params).toPath().toAbsolutePath();
+            }
+
+            @Override
+            public ApplicationLayout sourceApplicationLayout() {
+                return appImageLayout(params).resolveAt(
+                        applicationInstallDir(sourceRoot()));
+            }
+
+            @Override
+            public ApplicationLayout installedApplicationLayout() {
+                return appImageLayout(params).resolveAt(
+                        applicationInstallDir(Path.of("/")));
+            }
+
+            private Path applicationInstallDir(Path root) {
+                Path installDir = Path.of(LINUX_INSTALL_DIR.fetchFrom(params),
+                        name());
+                if (installDir.isAbsolute()) {
+                    installDir = Path.of("." + installDir.toString()).normalize();
+                }
+                return root.resolve(installDir);
+            }
+        };
+    }
+
+    private ApplicationLayout appImageLayout(
+            Map<String, ? super Object> params) {
+        if (StandardBundlerParam.isRuntimeInstaller(params)) {
+            return ApplicationLayout.javaRuntime();
+        }
+        return ApplicationLayout.unixApp();
+    }
+
+    private static void validateFileAssociations(
+            List<Map<String, ? super Object>> associations) throws
+            ConfigException {
+        // only one mime type per association, at least one file extention
+        int assocIdx = 0;
+        for (var assoc : associations) {
+            ++assocIdx;
+            List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
+            if (mimes == null || mimes.isEmpty()) {
+                String msgKey = "error.no-content-types-for-file-association";
+                throw new ConfigException(
+                        MessageFormat.format(I18N.getString(msgKey), assocIdx),
+                        I18N.getString(msgKey + ".advise"));
+
+            }
+
+            if (mimes.size() > 1) {
+                String msgKey = "error.too-many-content-types-for-file-association";
+                throw new ConfigException(
+                        MessageFormat.format(I18N.getString(msgKey), assocIdx),
+                        I18N.getString(msgKey + ".advise"));
+            }
+        }
+    }
+
+    /**
+     * Helper to create files for desktop integration.
+     */
+    private class DesktopIntegration {
+
+        DesktopIntegration(PlatformPackage thePackage,
+                Map<String, ? super Object> params) {
+
+            associations = FILE_ASSOCIATIONS.fetchFrom(params).stream().filter(
+                    a -> {
+                        if (a == null) {
+                            return false;
+                        }
+                        List<String> mimes = FA_CONTENT_TYPE.fetchFrom(a);
+                        return (mimes != null && !mimes.isEmpty());
+                    }).collect(Collectors.toUnmodifiableList());
+
+            launchers = ADD_LAUNCHERS.fetchFrom(params);
+
+            this.thePackage = thePackage;
+
+            customIconFile = ICON_PNG.fetchFrom(params);
+
+            verbose = VERBOSE.fetchFrom(params);
+            resourceDir = RESOURCE_DIR.fetchFrom(params);
+
+            // XDG recommends to use vendor prefix in desktop file names as xdg
+            // commands copy files to system directories.
+            // Package name should be a good prefix.
+            final String desktopFileName = String.format("%s-%s.desktop",
+                        thePackage.name(), APP_NAME.fetchFrom(params));
+            final String mimeInfoFileName = String.format("%s-%s-MimeInfo.xml",
+                        thePackage.name(), APP_NAME.fetchFrom(params));
+
+            mimeInfoFile = new DesktopFile(mimeInfoFileName);
+
+            if (!associations.isEmpty() || SHORTCUT_HINT.fetchFrom(params) || customIconFile != null) {
+                //
+                // Create primary .desktop file if one of conditions is met:
+                // - there are file associations configured
+                // - user explicitely requested to create a shortcut
+                // - custom icon specified
+                //
+                desktopFile = new DesktopFile(desktopFileName);
+                iconFile = new DesktopFile(String.format("%s.png",
+                        APP_NAME.fetchFrom(params)));
+            } else {
+                desktopFile = null;
+                iconFile = null;
+            }
+
+            this.desktopFileData = Collections.unmodifiableMap(
+                    createDataForDesktopFile(params));
+        }
+
+        Map<String, String> prepareForApplication() throws IOException {
+            if (iconFile != null) {
+                // Create application icon file.
+                prepareSrcIconFile();
+            }
+
+            Map<String, String> data = new HashMap<>(desktopFileData);
+
+            final ShellCommands shellCommands;
+            if (desktopFile != null) {
+                // Create application desktop description file.
+                createDesktopFile(data);
+
+                // Shell commands will be created only if desktop file
+                // should be installed.
+                shellCommands = new ShellCommands();
+            } else {
+                shellCommands = null;
+            }
+
+            if (!associations.isEmpty()) {
+                // Create XML file with mime types corresponding to file associations.
+                createFileAssociationsMimeInfoFile();
+
+                shellCommands.setFileAssociations();
+
+                // Create icon files corresponding to file associations
+                Map<String, Path> mimeTypeWithIconFile = createFileAssociationIconFiles();
+                mimeTypeWithIconFile.forEach((k, v) -> {
+                    shellCommands.addIcon(k, v);
+                });
+            }
+
+            // Create shell commands to install/uninstall integration with desktop of the app.
+            if (shellCommands != null) {
+                shellCommands.applyTo(data);
+            }
+
+            boolean needCleanupScripts = !associations.isEmpty();
+
+            // Take care of additional launchers if there are any.
+            // Process every additional launcher as the main application launcher.
+            // Collect shell commands to install/uninstall integration with desktop
+            // of the additional launchers and append them to the corresponding
+            // commands of the main launcher.
+            List<String> installShellCmds = new ArrayList<>(Arrays.asList(
+                    data.get(DESKTOP_COMMANDS_INSTALL)));
+            List<String> uninstallShellCmds = new ArrayList<>(Arrays.asList(
+                    data.get(DESKTOP_COMMANDS_UNINSTALL)));
+            for (Map<String, ? super Object> params : launchers) {
+                DesktopIntegration integration = new DesktopIntegration(
+                        thePackage, params);
+
+                if (!integration.associations.isEmpty()) {
+                    needCleanupScripts = true;
+                }
+
+                Map<String, String> launcherData = integration.prepareForApplication();
+
+                installShellCmds.add(launcherData.get(DESKTOP_COMMANDS_INSTALL));
+                uninstallShellCmds.add(launcherData.get(
+                        DESKTOP_COMMANDS_UNINSTALL));
+            }
+
+            data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands(
+                    installShellCmds));
+            data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands(
+                    uninstallShellCmds));
+
+            if (needCleanupScripts) {
+                // Pull in utils.sh scrips library.
+                try (InputStream is = getResourceAsStream("utils.sh");
+                        InputStreamReader isr = new InputStreamReader(is);
+                        BufferedReader reader = new BufferedReader(isr)) {
+                    data.put(UTILITY_SCRIPTS, reader.lines().collect(
+                            Collectors.joining(System.lineSeparator())));
+                }
+            } else {
+                data.put(UTILITY_SCRIPTS, "");
+            }
+
+            return data;
+        }
+
+        private Map<String, String> createDataForDesktopFile(
+                Map<String, ? super Object> params) {
+            Map<String, String> data = new HashMap<>();
+            data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params));
+            data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
+            data.put("APPLICATION_ICON",
+                    iconFile != null ? iconFile.installPath().toString() : null);
+            data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params));
+            data.put("APPLICATION_LAUNCHER",
+                    thePackage.installedApplicationLayout().launchersDirectory().resolve(
+                            LinuxAppImageBuilder.getLauncherName(params)).toString());
+
+            return data;
+        }
+
+        /**
+         * Shell commands to integrate something with desktop.
+         */
+        private class ShellCommands {
+
+            ShellCommands() {
+                registerIconCmds = new ArrayList<>();
+                unregisterIconCmds = new ArrayList<>();
+
+                registerDesktopFileCmd = String.join(" ", "xdg-desktop-menu",
+                        "install", desktopFile.installPath().toString());
+                unregisterDesktopFileCmd = String.join(" ", "xdg-desktop-menu",
+                        "uninstall", desktopFile.installPath().toString());
+            }
+
+            void setFileAssociations() {
+                registerFileAssociationsCmd = String.join(" ", "xdg-mime",
+                        "install",
+                        mimeInfoFile.installPath().toString());
+                unregisterFileAssociationsCmd = String.join(" ", "xdg-mime",
+                        "uninstall", mimeInfoFile.installPath().toString());
+
+                //
+                // Add manual cleanup of system files to get rid of
+                // the default mime type handlers.
+                //
+                // Even after mime type is unregisterd with `xdg-mime uninstall`
+                // command and desktop file deleted with `xdg-desktop-menu uninstall`
+                // command, records in
+                // `/usr/share/applications/defaults.list` (Ubuntu 16) or
+                // `/usr/local/share/applications/defaults.list` (OracleLinux 7)
+                // files remain referencing deleted mime time and deleted
+                // desktop file which makes `xdg-mime query default` output name
+                // of non-existing desktop file.
+                //
+                String cleanUpCommand = String.join(" ",
+                        "uninstall_default_mime_handler",
+                        desktopFile.installPath().getFileName().toString(),
+                        String.join(" ", getMimeTypeNamesFromFileAssociations()));
+
+                unregisterFileAssociationsCmd = stringifyShellCommands(
+                        unregisterFileAssociationsCmd, cleanUpCommand);
+            }
+
+            void addIcon(String mimeType, Path iconFile) {
+                final int imgSize = getSquareSizeOfImage(iconFile.toFile());
+                final String dashMime = mimeType.replace('/', '-');
+                registerIconCmds.add(String.join(" ", "xdg-icon-resource",
+                        "install", "--context", "mimetypes", "--size ",
+                        Integer.toString(imgSize), iconFile.toString(), dashMime));
+                unregisterIconCmds.add(String.join(" ", "xdg-icon-resource",
+                        "uninstall", dashMime));
+            }
+
+            void applyTo(Map<String, String> data) {
+                List<String> cmds = new ArrayList<>();
+
+                cmds.add(registerDesktopFileCmd);
+                cmds.add(registerFileAssociationsCmd);
+                cmds.addAll(registerIconCmds);
+                data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands(cmds));
+
+                cmds.clear();
+                cmds.add(unregisterDesktopFileCmd);
+                cmds.add(unregisterFileAssociationsCmd);
+                cmds.addAll(unregisterIconCmds);
+                data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands(cmds));
+            }
+
+            private String registerDesktopFileCmd;
+            private String unregisterDesktopFileCmd;
+
+            private String registerFileAssociationsCmd;
+            private String unregisterFileAssociationsCmd;
+
+            private List<String> registerIconCmds;
+            private List<String> unregisterIconCmds;
+        }
+
+        private final PlatformPackage thePackage;
+
+        private final List<Map<String, ? super Object>> associations;
+
+        private final List<Map<String, ? super Object>> launchers;
+
+        /**
+         * Desktop integration file. xml, icon, etc.
+         * Resides somewhere in application installation tree.
+         * Has two paths:
+         *  - path where it should be placed at package build time;
+         *  - path where it should be installed by package manager;
+         */
+        private class DesktopFile {
+
+            DesktopFile(String fileName) {
+                installPath = thePackage
+                        .installedApplicationLayout()
+                        .destktopIntegrationDirectory().resolve(fileName);
+                srcPath = thePackage
+                        .sourceApplicationLayout()
+                        .destktopIntegrationDirectory().resolve(fileName);
+            }
+
+            private final Path installPath;
+            private final Path srcPath;
+
+            Path installPath() {
+                return installPath;
+            }
+
+            Path srcPath() {
+                return srcPath;
+            }
+        }
+
+        private final boolean verbose;
+        private final File resourceDir;
+
+        private final DesktopFile mimeInfoFile;
+        private final DesktopFile desktopFile;
+        private final DesktopFile iconFile;
+
+        private final Map<String, String> desktopFileData;
+
+        /**
+         * Path to icon file provided by user or null.
+         */
+        private final File customIconFile;
+
+        private void appendFileAssociation(XMLStreamWriter xml,
+                Map<String, ? super Object> assoc) throws XMLStreamException {
+
+            xml.writeStartElement("mime-type");
+            final String thisMime = FA_CONTENT_TYPE.fetchFrom(assoc).get(0);
+            xml.writeAttribute("type", thisMime);
+
+            final String description = FA_DESCRIPTION.fetchFrom(assoc);
+            if (description != null && !description.isEmpty()) {
+                xml.writeStartElement("comment");
+                xml.writeCharacters(description);
+                xml.writeEndElement();
+            }
+
+            final List<String> extensions = FA_EXTENSIONS.fetchFrom(assoc);
+            if (extensions == null) {
+                Log.error(I18N.getString(
+                        "message.creating-association-with-null-extension"));
+            } else {
+                for (String ext : extensions) {
+                    xml.writeStartElement("glob");
+                    xml.writeAttribute("pattern", "*." + ext);
+                    xml.writeEndElement();
+                }
+            }
+
+            xml.writeEndElement();
+        }
+
+        private void createFileAssociationsMimeInfoFile() throws IOException {
+            XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
+
+            try (Writer w = new BufferedWriter(new FileWriter(
+                    mimeInfoFile.srcPath().toFile()))) {
+                XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w);
+
+                xml.writeStartDocument();
+                xml.writeStartElement("mime-info");
+                xml.writeNamespace("xmlns",
+                        "http://www.freedesktop.org/standards/shared-mime-info");
+
+                for (var assoc : associations) {
+                    appendFileAssociation(xml, assoc);
+                }
+
+                xml.writeEndElement();
+                xml.writeEndDocument();
+                xml.flush();
+                xml.close();
+
+            } catch (XMLStreamException ex) {
+                Log.verbose(ex);
+                throw new IOException(ex);
+            }
+        }
+
+        private Map<String, Path> createFileAssociationIconFiles() throws
+                IOException {
+            Map<String, Path> mimeTypeWithIconFile = new HashMap<>();
+            for (var assoc : associations) {
+                File customFaIcon = FA_ICON.fetchFrom(assoc);
+                if (customFaIcon == null || !customFaIcon.exists() || getSquareSizeOfImage(
+                        customFaIcon) == 0) {
+                    continue;
+                }
+
+                String fname = iconFile.srcPath().getFileName().toString();
+                if (fname.indexOf(".") > 0) {
+                    fname = fname.substring(0, fname.lastIndexOf("."));
+                }
+
+                DesktopFile faIconFile = new DesktopFile(
+                        fname + "_fa_" + customFaIcon.getName());
+
+                IOUtils.copyFile(customFaIcon, faIconFile.srcPath().toFile());
+
+                mimeTypeWithIconFile.put(FA_CONTENT_TYPE.fetchFrom(assoc).get(0),
+                        faIconFile.installPath());
+            }
+            return mimeTypeWithIconFile;
+        }
+
+        private void createDesktopFile(Map<String, String> data) throws IOException {
+            List<String> mimeTypes = getMimeTypeNamesFromFileAssociations();
+            data.put("DESKTOP_MIMES", "MimeType=" + String.join(";", mimeTypes));
+
+            // prepare desktop shortcut
+            try (Writer w = Files.newBufferedWriter(desktopFile.srcPath())) {
+                String content = preprocessTextResource(
+                        desktopFile.srcPath().getFileName().toString(),
+                        I18N.getString("resource.menu-shortcut-descriptor"),
+                        "template.desktop",
+                        data,
+                        verbose,
+                        resourceDir);
+                w.write(content);
+            }
+        }
+
+        private void prepareSrcIconFile() throws IOException {
+            if (customIconFile == null || !customIconFile.exists()) {
+                fetchResource(iconFile.srcPath().getFileName().toString(),
+                        I18N.getString("resource.menu-icon"),
+                        DEFAULT_ICON,
+                        iconFile.srcPath().toFile(),
+                        verbose,
+                        resourceDir);
+            } else {
+                fetchResource(iconFile.srcPath().getFileName().toString(),
+                        I18N.getString("resource.menu-icon"),
+                        customIconFile,
+                        iconFile.srcPath().toFile(),
+                        verbose,
+                        resourceDir);
+            }
+        }
+
+        private List<String> getMimeTypeNamesFromFileAssociations() {
+            return associations.stream().map(
+                    a -> FA_CONTENT_TYPE.fetchFrom(a).get(0)).collect(
+                            Collectors.toUnmodifiableList());
+        }
+    }
+
+    private static int getSquareSizeOfImage(File f) {
+        try {
+            BufferedImage bi = ImageIO.read(f);
+            if (bi.getWidth() == bi.getHeight()) {
+                return bi.getWidth();
+            }
+        } catch (IOException e) {
+            Log.verbose(e);
+        }
+        return 0;
+    }
+
+    private static String stringifyShellCommands(String ... commands) {
+        return stringifyShellCommands(Arrays.asList(commands));
+    }
+
+    private static String stringifyShellCommands(List<String> commands) {
+        return String.join(System.lineSeparator(), commands.stream().filter(
+                s -> s != null && !s.isEmpty()).collect(Collectors.toList()));
+    }
+}
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java	Tue Sep 24 13:41:16 2019 -0400
@@ -25,10 +25,9 @@
 
 package jdk.jpackage.internal;
 
-import javax.imageio.ImageIO;
-import java.awt.image.BufferedImage;
 import java.io.*;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.text.MessageFormat;
 import java.util.*;
 import java.util.regex.Matcher;
@@ -36,7 +35,6 @@
 
 import static jdk.jpackage.internal.StandardBundlerParam.*;
 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
-import static jdk.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES;
 
 /**
  * There are two command line options to configure license information for RPM
@@ -49,28 +47,7 @@
  * to set license information. --license-file makes little sense in case of RPM
  * packaging.
  */
-public class LinuxRpmBundler extends AbstractBundler {
-
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-            "jdk.jpackage.internal.resources.LinuxResources");
-
-    public static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER =
-            new StandardBundlerParam<>(
-            "linux.app.bundler",
-            LinuxAppBundler.class,
-            params -> new LinuxAppBundler(),
-            null);
-
-    public static final BundlerParamInfo<File> RPM_IMAGE_DIR =
-            new StandardBundlerParam<>(
-            "linux.rpm.imageDir",
-            File.class,
-            params -> {
-                File imagesRoot = IMAGES_ROOT.fetchFrom(params);
-                if (!imagesRoot.exists()) imagesRoot.mkdirs();
-                return new File(imagesRoot, "linux-rpm.image");
-            },
-            (s, p) -> new File(s));
+public class LinuxRpmBundler extends LinuxPackageBundler {
 
     // Fedora rules for package naming are used here
     // https://fedoraproject.org/wiki/Packaging:NamingGuidelines?rd=Packaging/NamingGuidelines
@@ -80,10 +57,10 @@
     //
     // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._+
     //
-    private static final Pattern RPM_BUNDLE_NAME_PATTERN =
+    private static final Pattern RPM_PACKAGE_NAME_PATTERN =
             Pattern.compile("[a-z\\d\\+\\-\\.\\_]+", Pattern.CASE_INSENSITIVE);
 
-    public static final BundlerParamInfo<String> BUNDLE_NAME =
+    public static final BundlerParamInfo<String> PACKAGE_NAME =
             new StandardBundlerParam<> (
             Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(),
             String.class,
@@ -97,7 +74,7 @@
                 return nm;
             },
             (s, p) -> {
-                if (!RPM_BUNDLE_NAME_PATTERN.matcher(s).matches()) {
+                if (!RPM_PACKAGE_NAME_PATTERN.matcher(s).matches()) {
                     String msgKey = "error.invalid-value-for-package-name";
                     throw new IllegalArgumentException(
                             new ConfigException(MessageFormat.format(
@@ -109,14 +86,6 @@
             }
         );
 
-    public static final BundlerParamInfo<String> MENU_GROUP =
-        new StandardBundlerParam<>(
-                Arguments.CLIOptions.LINUX_MENU_GROUP.getId(),
-                String.class,
-                params -> I18N.getString("param.menu-group.default"),
-                (s, p) -> s
-        );
-
     public static final BundlerParamInfo<String> LICENSE_TYPE =
         new StandardBundlerParam<>(
                 Arguments.CLIOptions.LINUX_RPM_LICENSE_TYPE.getId(),
@@ -132,41 +101,7 @@
             params -> null,
             (s, p) -> s);
 
-    public static final BundlerParamInfo<String> XDG_FILE_PREFIX =
-            new StandardBundlerParam<> (
-            "linux.xdg-prefix",
-            String.class,
-            params -> {
-                try {
-                    String vendor;
-                    if (params.containsKey(VENDOR.getID())) {
-                        vendor = VENDOR.fetchFrom(params);
-                    } else {
-                        vendor = "jpackage";
-                    }
-                    String appName = APP_NAME.fetchFrom(params);
-
-                    return (vendor + "-" + appName).replaceAll("\\s", "");
-                } catch (Exception e) {
-                    Log.verbose(e);
-                }
-                return "unknown-MimeInfo.xml";
-            },
-            (s, p) -> s);
-
-    public static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
-        new StandardBundlerParam<>(
-                Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId(),
-                Boolean.class,
-                params -> false,
-                (s, p) -> (s == null || "null".equalsIgnoreCase(s))
-                        ? false : Boolean.valueOf(s)
-        );
-
-    private final static String DEFAULT_ICON = "java32.png";
     private final static String DEFAULT_SPEC_TEMPLATE = "template.spec";
-    private final static String DEFAULT_DESKTOP_FILE_TEMPLATE =
-            "template.desktop";
 
     public final static String TOOL_RPMBUILD = "rpmbuild";
     public final static double TOOL_RPMBUILD_MIN_VERSION = 4.0d;
@@ -195,430 +130,72 @@
         }
     }
 
-    @Override
-    public boolean validate(Map<String, ? super Object> params)
-            throws ConfigException {
-        try {
-            if (params == null) throw new ConfigException(
-                    I18N.getString("error.parameters-null"),
-                    I18N.getString("error.parameters-null.advice"));
-
-            // run basic validation to ensure requirements are met
-            // we are not interested in return code, only possible exception
-            APP_BUNDLER.fetchFrom(params).validate(params);
-
-            // validate presense of required tools
-            if (!testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)){
-                throw new ConfigException(
-                    MessageFormat.format(
-                        I18N.getString("error.cannot-find-rpmbuild"),
-                        TOOL_RPMBUILD_MIN_VERSION),
-                    MessageFormat.format(
-                        I18N.getString("error.cannot-find-rpmbuild.advice"),
-                        TOOL_RPMBUILD_MIN_VERSION));
-            }
-
-            // only one mime type per association, at least one file extension
-            List<Map<String, ? super Object>> associations =
-                    FILE_ASSOCIATIONS.fetchFrom(params);
-            if (associations != null) {
-                for (int i = 0; i < associations.size(); i++) {
-                    Map<String, ? super Object> assoc = associations.get(i);
-                    List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
-                    if (mimes == null || mimes.isEmpty()) {
-                        String msgKey =
-                                "error.no-content-types-for-file-association";
-                        throw new ConfigException(
-                                MessageFormat.format(I18N.getString(msgKey), i),
-                                I18N.getString(msgKey + ".advice"));
-                    } else if (mimes.size() > 1) {
-                        String msgKey =
-                                "error.no-content-types-for-file-association";
-                        throw new ConfigException(
-                                MessageFormat.format(I18N.getString(msgKey), i),
-                                I18N.getString(msgKey + ".advice"));
-                    }
-                }
-            }
-
-            // bundle name has some restrictions
-            // the string converter will throw an exception if invalid
-            BUNDLE_NAME.getStringConverter().apply(
-                    BUNDLE_NAME.fetchFrom(params), params);
-
-            return true;
-        } catch (RuntimeException re) {
-            if (re.getCause() instanceof ConfigException) {
-                throw (ConfigException) re.getCause();
-            } else {
-                throw new ConfigException(re);
-            }
-        }
+    public LinuxRpmBundler() {
+        super(PACKAGE_NAME);
     }
 
-    private boolean prepareProto(Map<String, ? super Object> params)
-            throws PackagerException, IOException {
-        File appImage = StandardBundlerParam.getPredefinedAppImage(params);
-        File appDir = null;
-
-        // we either have an application image or need to build one
-        if (appImage != null) {
-            appDir = new File(RPM_IMAGE_DIR.fetchFrom(params),
-                APP_NAME.fetchFrom(params));
-            // copy everything from appImage dir into appDir/name
-            IOUtils.copyRecursive(appImage.toPath(), appDir.toPath());
-        } else {
-            appDir = APP_BUNDLER.fetchFrom(params).doBundle(params,
-                    RPM_IMAGE_DIR.fetchFrom(params), true);
-        }
-        return appDir != null;
-    }
+    @Override
+    public void doValidate(Map<String, ? super Object> params)
+            throws ConfigException {
+        if (params == null) throw new ConfigException(
+                I18N.getString("error.parameters-null"),
+                I18N.getString("error.parameters-null.advice"));
 
-    public File bundle(Map<String, ? super Object> params,
-            File outdir) throws PackagerException {
-
-        IOUtils.writableOutputDir(outdir.toPath());
-
-        File imageDir = RPM_IMAGE_DIR.fetchFrom(params);
-        try {
-
-            imageDir.mkdirs();
-
-            if (prepareProto(params) && prepareProjectConfig(params)) {
-                return buildRPM(params, outdir);
-            }
-            return null;
-        } catch (IOException ex) {
-            Log.verbose(ex);
-            throw new PackagerException(ex);
+        // validate presense of required tools
+        if (!testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)){
+            throw new ConfigException(
+                MessageFormat.format(
+                    I18N.getString("error.cannot-find-rpmbuild"),
+                    TOOL_RPMBUILD_MIN_VERSION),
+                MessageFormat.format(
+                    I18N.getString("error.cannot-find-rpmbuild.advice"),
+                    TOOL_RPMBUILD_MIN_VERSION));
         }
     }
 
-    private boolean prepareProjectConfig(Map<String, ? super Object> params)
-            throws IOException {
-        Map<String, String> data = createReplacementData(params);
-        File rootDir =
-            LinuxAppBundler.getRootDir(RPM_IMAGE_DIR.fetchFrom(params), params);
-        File binDir = new File(rootDir, "bin");
-
-        // prepare installer icon
-        File iconTarget = getConfig_IconFile(binDir, params);
-        File icon = LinuxAppBundler.ICON_PNG.fetchFrom(params);
-        if (!StandardBundlerParam.isRuntimeInstaller(params)) {
-            if (icon == null || !icon.exists()) {
-                fetchResource(iconTarget.getName(),
-                        I18N.getString("resource.menu-icon"),
-                        DEFAULT_ICON,
-                        iconTarget,
-                        VERBOSE.fetchFrom(params),
-                        RESOURCE_DIR.fetchFrom(params));
-            } else {
-                fetchResource(iconTarget.getName(),
-                        I18N.getString("resource.menu-icon"),
-                        icon,
-                        iconTarget,
-                        VERBOSE.fetchFrom(params),
-                        RESOURCE_DIR.fetchFrom(params));
-            }
-        }
-
-        StringBuilder installScripts = new StringBuilder();
-        StringBuilder removeScripts = new StringBuilder();
-        for (Map<String, ? super Object> addLauncher :
-                ADD_LAUNCHERS.fetchFrom(params)) {
-            Map<String, String> addLauncherData =
-                    createReplacementData(addLauncher);
-            addLauncherData.put("APPLICATION_FS_NAME",
-                    data.get("APPLICATION_FS_NAME"));
-            addLauncherData.put("DESKTOP_MIMES", "");
-
-            // prepare desktop shortcut
-            if (SHORTCUT_HINT.fetchFrom(params)) {
-                try (Writer w = Files.newBufferedWriter(
-                    getConfig_DesktopShortcutFile(binDir,
-                            addLauncher).toPath())) {
-                    String content = preprocessTextResource(
-                        getConfig_DesktopShortcutFile(binDir,
-                        addLauncher).getName(),
-                        I18N.getString("resource.menu-shortcut-descriptor"),
-                        DEFAULT_DESKTOP_FILE_TEMPLATE, addLauncherData,
-                        VERBOSE.fetchFrom(params),
-                        RESOURCE_DIR.fetchFrom(params));
-                    w.write(content);
-                }
-            }
-
-            // prepare installer icon
-            iconTarget = getConfig_IconFile(binDir, addLauncher);
-            icon = LinuxAppBundler.ICON_PNG.fetchFrom(addLauncher);
-            if (icon == null || !icon.exists()) {
-                fetchResource(iconTarget.getName(),
-                        I18N.getString("resource.menu-icon"),
-                        DEFAULT_ICON,
-                        iconTarget,
-                        VERBOSE.fetchFrom(params),
-                        RESOURCE_DIR.fetchFrom(params));
-            } else {
-                fetchResource(iconTarget.getName(),
-                        I18N.getString("resource.menu-icon"),
-                        icon,
-                        iconTarget,
-                        VERBOSE.fetchFrom(params),
-                        RESOURCE_DIR.fetchFrom(params));
-            }
-
-            // post copying of desktop icon
-            installScripts.append("xdg-desktop-menu install --novendor ");
-            installScripts.append(LINUX_INSTALL_DIR.fetchFrom(params));
-            installScripts.append("/");
-            installScripts.append(data.get("APPLICATION_FS_NAME"));
-            installScripts.append("/bin/");
-            installScripts.append(addLauncherData.get(
-                    "APPLICATION_LAUNCHER_FILENAME"));
-            installScripts.append(".desktop\n");
-
-            // preun cleanup of desktop icon
-            removeScripts.append("xdg-desktop-menu uninstall --novendor ");
-            removeScripts.append(LINUX_INSTALL_DIR.fetchFrom(params));
-            removeScripts.append("/");
-            removeScripts.append(data.get("APPLICATION_FS_NAME"));
-            removeScripts.append("/bin/");
-            removeScripts.append(addLauncherData.get(
-                    "APPLICATION_LAUNCHER_FILENAME"));
-            removeScripts.append(".desktop\n");
-
-        }
-        data.put("ADD_LAUNCHERS_INSTALL", installScripts.toString());
-        data.put("ADD_LAUNCHERS_REMOVE", removeScripts.toString());
-
-        StringBuilder cdsScript = new StringBuilder();
-
-        data.put("APP_CDS_CACHE", cdsScript.toString());
-
-        List<Map<String, ? super Object>> associations =
-                FILE_ASSOCIATIONS.fetchFrom(params);
-        data.put("FILE_ASSOCIATION_INSTALL", "");
-        data.put("FILE_ASSOCIATION_REMOVE", "");
-        data.put("DESKTOP_MIMES", "");
-        if (associations != null) {
-            String mimeInfoFile = XDG_FILE_PREFIX.fetchFrom(params)
-                    + "-MimeInfo.xml";
-            StringBuilder mimeInfo = new StringBuilder(
-                "<?xml version=\"1.0\"?>\n<mime-info xmlns="
-                +"'http://www.freedesktop.org/standards/shared-mime-info'>\n");
-            StringBuilder registrations = new StringBuilder();
-            StringBuilder deregistrations = new StringBuilder();
-            StringBuilder desktopMimes = new StringBuilder("MimeType=");
-            boolean addedEntry = false;
-
-            for (Map<String, ? super Object> assoc : associations) {
-                //  <mime-type type="application/x-vnd.awesome">
-                //    <comment>Awesome document</comment>
-                //    <glob pattern="*.awesome"/>
-                //    <glob pattern="*.awe"/>
-                //  </mime-type>
-
-                if (assoc == null) {
-                    continue;
-                }
+    @Override
+    protected File buildPackageBundle(
+            Map<String, String> replacementData,
+            Map<String, ? super Object> params, File outputParentDir) throws
+            PackagerException, IOException {
 
-                String description = FA_DESCRIPTION.fetchFrom(assoc);
-                File faIcon = FA_ICON.fetchFrom(assoc);
-                List<String> extensions = FA_EXTENSIONS.fetchFrom(assoc);
-                if (extensions == null) {
-                    Log.verbose(I18N.getString(
-                        "message.creating-association-with-null-extension"));
-                }
-
-                List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
-                if (mimes == null || mimes.isEmpty()) {
-                    continue;
-                }
-                String thisMime = mimes.get(0);
-                String dashMime = thisMime.replace('/', '-');
-
-                mimeInfo.append("  <mime-type type='")
-                        .append(thisMime)
-                        .append("'>\n");
-                if (description != null && !description.isEmpty()) {
-                    mimeInfo.append("    <comment>")
-                            .append(description)
-                            .append("</comment>\n");
-                }
-
-                if (extensions != null) {
-                    for (String ext : extensions) {
-                        mimeInfo.append("    <glob pattern='*.")
-                                .append(ext)
-                                .append("'/>\n");
-                    }
-                }
-
-                mimeInfo.append("  </mime-type>\n");
-                if (!addedEntry) {
-                    registrations.append("xdg-mime install ")
-                            .append(LINUX_INSTALL_DIR.fetchFrom(params))
-                            .append("/")
-                            .append(data.get("APPLICATION_FS_NAME"))
-                            .append("/bin/")
-                            .append(mimeInfoFile)
-                            .append("\n");
-
-                    deregistrations.append("xdg-mime uninstall ")
-                            .append(LINUX_INSTALL_DIR.fetchFrom(params))
-                            .append("/")
-                            .append(data.get("APPLICATION_FS_NAME"))
-                            .append("/bin/")
-                            .append(mimeInfoFile)
-                            .append("\n");
-                    addedEntry = true;
-                } else {
-                    desktopMimes.append(";");
-                }
-                desktopMimes.append(thisMime);
-
-                if (faIcon != null && faIcon.exists()) {
-                    int size = getSquareSizeOfImage(faIcon);
-
-                    if (size > 0) {
-                        File target = new File(binDir,
-                                APP_NAME.fetchFrom(params)
-                                + "_fa_" + faIcon.getName());
-                        IOUtils.copyFile(faIcon, target);
-
-                        // xdg-icon-resource install --context mimetypes
-                        // --size 64 awesomeapp_fa_1.png
-                        // application-x.vnd-awesome
-                        registrations.append(
-                                "xdg-icon-resource install "
-                                + "--context mimetypes --size ")
-                                .append(size)
-                                .append(" ")
-                                .append(LINUX_INSTALL_DIR.fetchFrom(params))
-                                .append("/")
-                                .append(data.get("APPLICATION_FS_NAME"))
-                                .append("/")
-                                .append(target.getName())
-                                .append(" ")
-                                .append(dashMime)
-                                .append("\n");
-
-                        // xdg-icon-resource uninstall --context mimetypes
-                        // --size 64 awesomeapp_fa_1.png
-                        // application-x.vnd-awesome
-                        deregistrations.append(
-                                "xdg-icon-resource uninstall "
-                                + "--context mimetypes --size ")
-                                .append(size)
-                                .append(" ")
-                                .append(LINUX_INSTALL_DIR.fetchFrom(params))
-                                .append("/")
-                                .append(data.get("APPLICATION_FS_NAME"))
-                                .append("/")
-                                .append(target.getName())
-                                .append(" ")
-                                .append(dashMime)
-                                .append("\n");
-                    }
-                }
-            }
-            mimeInfo.append("</mime-info>");
-
-            if (addedEntry) {
-                try (Writer w = Files.newBufferedWriter(
-                        new File(binDir, mimeInfoFile).toPath())) {
-                    w.write(mimeInfo.toString());
-                }
-                data.put("FILE_ASSOCIATION_INSTALL", registrations.toString());
-                data.put("FILE_ASSOCIATION_REMOVE", deregistrations.toString());
-                data.put("DESKTOP_MIMES", desktopMimes.toString());
-            }
-        }
-
-        if (!StandardBundlerParam.isRuntimeInstaller(params)) {
-            // prepare desktop shortcut
-            if (SHORTCUT_HINT.fetchFrom(params)) {
-                try (Writer w = Files.newBufferedWriter(
-                    getConfig_DesktopShortcutFile(binDir, params).toPath())) {
-                    String content = preprocessTextResource(
-                        getConfig_DesktopShortcutFile(binDir,
-                                                          params).getName(),
-                    I18N.getString("resource.menu-shortcut-descriptor"),
-                        DEFAULT_DESKTOP_FILE_TEMPLATE, data,
-                        VERBOSE.fetchFrom(params),
-                        RESOURCE_DIR.fetchFrom(params));
-                    w.write(content);
-                }
-            }
-        }
+        Path specFile = specFile(params);
 
         // prepare spec file
-        try (Writer w = Files.newBufferedWriter(
-                getConfig_SpecFile(params).toPath())) {
+        Files.createDirectories(specFile.getParent());
+        try (Writer w = Files.newBufferedWriter(specFile)) {
             String content = preprocessTextResource(
-                    getConfig_SpecFile(params).getName(),
+                    specFile.getFileName().toString(),
                     I18N.getString("resource.rpm-spec-file"),
-                    DEFAULT_SPEC_TEMPLATE, data,
+                    DEFAULT_SPEC_TEMPLATE, replacementData,
                     VERBOSE.fetchFrom(params),
                     RESOURCE_DIR.fetchFrom(params));
             w.write(content);
         }
 
-        return true;
+        return buildRPM(params, outputParentDir);
     }
 
-    private Map<String, String> createReplacementData(
+    @Override
+    protected Map<String, String> createReplacementData(
             Map<String, ? super Object> params) throws IOException {
         Map<String, String> data = new HashMap<>();
-        String launcher = LinuxAppImageBuilder.getLauncherRelativePath(params);
 
-        data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params));
-        data.put("APPLICATION_FS_NAME", APP_NAME.fetchFrom(params));
-        data.put("APPLICATION_PACKAGE", BUNDLE_NAME.fetchFrom(params));
-        data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
-        data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
-        data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params));
-        data.put("APPLICATION_LAUNCHER_FILENAME", launcher);
-        data.put("INSTALLATION_DIRECTORY", LINUX_INSTALL_DIR.fetchFrom(params));
-        data.put("XDG_PREFIX", XDG_FILE_PREFIX.fetchFrom(params));
-        data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params));
-        data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
+        data.put("APPLICATION_DIRECTORY", Path.of(LINUX_INSTALL_DIR.fetchFrom(
+                params), PACKAGE_NAME.fetchFrom(params)).toString());
         data.put("APPLICATION_SUMMARY", APP_NAME.fetchFrom(params));
         data.put("APPLICATION_LICENSE_TYPE", LICENSE_TYPE.fetchFrom(params));
-
-        String licenseFile = LICENSE_FILE.fetchFrom(params);
-        if (licenseFile == null) {
-            licenseFile = "";
-        }
-        data.put("APPLICATION_LICENSE_FILE", licenseFile);
+        data.put("APPLICATION_LICENSE_FILE", Optional.ofNullable(
+                LICENSE_FILE.fetchFrom(params)).orElse(""));
+        data.put("APPLICATION_GROUP", Optional.ofNullable(
+                GROUP.fetchFrom(params)).orElse(""));
 
-        String group = GROUP.fetchFrom(params);
-        if (group == null) {
-            group = "";
-        }
-        data.put("APPLICATION_GROUP", group);
-
-        String deps = LINUX_PACKAGE_DEPENDENCIES.fetchFrom(params);
-        data.put("PACKAGE_DEPENDENCIES",
-                deps.isEmpty() ? "" : "Requires: " + deps);
-        data.put("RUNTIME_INSTALLER", "" +
-                StandardBundlerParam.isRuntimeInstaller(params));
         return data;
     }
 
-    private File getConfig_DesktopShortcutFile(File rootDir,
-            Map<String, ? super Object> params) {
-        return new File(rootDir, APP_NAME.fetchFrom(params) + ".desktop");
-    }
-
-    private File getConfig_IconFile(File rootDir,
-            Map<String, ? super Object> params) {
-        return new File(rootDir, APP_NAME.fetchFrom(params) + ".png");
-    }
-
-    private File getConfig_SpecFile(Map<String, ? super Object> params) {
-        return new File(RPM_IMAGE_DIR.fetchFrom(params),
-                APP_NAME.fetchFrom(params) + ".spec");
+    private Path specFile(Map<String, ? super Object> params) {
+        return TEMP_ROOT.fetchFrom(params).toPath().resolve(Path.of("SPECS",
+                PACKAGE_NAME.fetchFrom(params) + ".spec"));
     }
 
     private File buildRPM(Map<String, ? super Object> params,
@@ -627,22 +204,19 @@
                 "message.outputting-bundle-location"),
                 outdir.getAbsolutePath()));
 
-        File broot = new File(TEMP_ROOT.fetchFrom(params), "rmpbuildroot");
-
-        outdir.mkdirs();
+        PlatformPackage thePackage = createMetaPackage(params);
 
         //run rpmbuild
         ProcessBuilder pb = new ProcessBuilder(
                 TOOL_RPMBUILD,
-                "-bb", getConfig_SpecFile(params).getAbsolutePath(),
-                "--define", "%_sourcedir "
-                        + RPM_IMAGE_DIR.fetchFrom(params).getAbsolutePath(),
+                "-bb", specFile(params).toAbsolutePath().toString(),
+                "--define", String.format("%%_sourcedir %s", thePackage.sourceRoot()),
                 // save result to output dir
-                "--define", "%_rpmdir " + outdir.getAbsolutePath(),
+                "--define", String.format("%%_rpmdir %s", outdir.getAbsolutePath()),
                 // do not use other system directories to build as current user
-                "--define", "%_topdir " + broot.getAbsolutePath()
+                "--define", String.format("%%_topdir %s",
+                        TEMP_ROOT.fetchFrom(params).toPath().toAbsolutePath())
         );
-        pb = pb.directory(RPM_IMAGE_DIR.fetchFrom(params));
         IOUtils.exec(pb);
 
         Log.verbose(MessageFormat.format(
@@ -678,22 +252,7 @@
     }
 
     @Override
-    public String getBundleType() {
-        return "INSTALLER";
-    }
-
-    @Override
-    public File execute(Map<String, ? super Object> params,
-            File outputParentDir) throws PackagerException {
-        return bundle(params, outputParentDir);
-    }
-
-    @Override
     public boolean supported(boolean runtimeInstaller) {
-        return isSupported();
-    }
-
-    public static boolean isSupported() {
         if (Platform.getPlatform() == Platform.LINUX) {
             if (testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)) {
                 return true;
@@ -702,20 +261,6 @@
         return false;
     }
 
-    public int getSquareSizeOfImage(File f) {
-        try {
-            BufferedImage bi = ImageIO.read(f);
-            if (bi.getWidth() == bi.getHeight()) {
-                return bi.getWidth();
-            } else {
-                return 0;
-            }
-        } catch (Exception e) {
-            e.printStackTrace();
-            return 0;
-        }
-    }
-
     @Override
     public boolean isDefault() {
         return !LinuxDebBundler.isDebian();
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.desktop	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.desktop	Tue Sep 24 13:41:16 2019 -0400
@@ -1,8 +1,8 @@
 [Desktop Entry]
 Name=APPLICATION_NAME
 Comment=APPLICATION_DESCRIPTION
-Exec=INSTALLATION_DIRECTORY/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
-Icon=INSTALLATION_DIRECTORY/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.png
+Exec=APPLICATION_LAUNCHER
+Icon=APPLICATION_ICON
 Terminal=false
 Type=Application
 Categories=DEPLOY_BUNDLE_CATEGORY
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.postinst	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.postinst	Tue Sep 24 13:41:16 2019 -0400
@@ -1,5 +1,5 @@
 #!/bin/sh
-# postinst script for APPLICATION_NAME
+# postinst script for APPLICATION_PACKAGE
 #
 # see: dh_installdeb(1)
 
@@ -19,12 +19,7 @@
 
 case "$1" in
     configure)
-        if [ "RUNTIME_INSTALLER" != "true" ]; then
-            echo Adding shortcut to the menu
-ADD_LAUNCHERS_INSTALL
-            xdg-desktop-menu install --novendor INSTALLATION_DIRECTORY/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
-FILE_ASSOCIATION_INSTALL
-        fi
+DESKTOP_COMMANDS_INSTALL
     ;;
 
     abort-upgrade|abort-remove|abort-deconfigure)
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.postrm	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.postrm	Tue Sep 24 13:41:16 2019 -0400
@@ -1,5 +1,5 @@
 #!/bin/sh
-# postrm script for APPLICATION_NAME
+# postrm script for APPLICATION_PACKAGE
 #
 # see: dh_installdeb(1)
 
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.preinst	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.preinst	Tue Sep 24 13:41:16 2019 -0400
@@ -1,5 +1,5 @@
 #!/bin/sh
-# preinst script for APPLICATION_NAME
+# preinst script for APPLICATION_PACKAGE
 #
 # see: dh_installdeb(1)
 
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.prerm	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.prerm	Tue Sep 24 13:41:16 2019 -0400
@@ -1,5 +1,5 @@
 #!/bin/sh
-# prerm script for APPLICATION_NAME
+# prerm script for APPLICATION_PACKAGE
 #
 # see: dh_installdeb(1)
 
@@ -17,14 +17,11 @@
 # the debian-policy package
 
 
+UTILITY_SCRIPTS
+
 case "$1" in
     remove|upgrade|deconfigure)
-        if [ "RUNTIME_INSTALLER" != "true" ]; then
-            echo Removing shortcut
-ADD_LAUNCHERS_REMOVE
-            xdg-desktop-menu uninstall --novendor INSTALLATION_DIRECTORY/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
-FILE_ASSOCIATION_REMOVE
-        fi
+DESKTOP_COMMANDS_UNINSTALL
     ;;
 
     failed-upgrade)
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.spec	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.spec	Tue Sep 24 13:41:16 2019 -0400
@@ -4,7 +4,7 @@
 Release: APPLICATION_RELEASE
 License: APPLICATION_LICENSE_TYPE
 Vendor: APPLICATION_VENDOR
-Prefix: INSTALLATION_DIRECTORY
+Prefix: %{dirname:APPLICATION_DIRECTORY}
 Provides: APPLICATION_PACKAGE
 %if "xAPPLICATION_GROUP" != x
 Group: APPLICATION_GROUP
@@ -12,7 +12,9 @@
 
 Autoprov: 0
 Autoreq: 0
-PACKAGE_DEPENDENCIES
+%if "xPACKAGE_DEPENDENCIES" != x
+Requires: PACKAGE_DEPENDENCIES
+%endif
 
 #avoid ARCH subfolder
 %define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm
@@ -31,8 +33,8 @@
 
 %install
 rm -rf %{buildroot}
-mkdir -p %{buildroot}INSTALLATION_DIRECTORY
-cp -r %{_sourcedir}/APPLICATION_FS_NAME %{buildroot}INSTALLATION_DIRECTORY
+install -d -m 755 %{buildroot}APPLICATION_DIRECTORY
+cp -r %{_sourcedir}APPLICATION_DIRECTORY/* %{buildroot}APPLICATION_DIRECTORY
 %if "xAPPLICATION_LICENSE_FILE" != x
   %define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:APPLICATION_LICENSE_FILE}
   install -d -m 755 %{buildroot}%{dirname:%{license_install_file}}
@@ -40,24 +42,20 @@
 %endif
 
 %files
-%{?license_install_file:%license %{license_install_file}}
+%if "xAPPLICATION_LICENSE_FILE" != x
+  %license %{license_install_file}
+  %{dirname:%{license_install_file}}
+%endif
 # If installation directory for the application is /a/b/c, we want only root
 # component of the path (/a) in the spec file to make sure all subdirectories
 # are owned by the package.
-%(echo INSTALLATION_DIRECTORY/APPLICATION_FS_NAME | sed -e "s|\(^/[^/]\{1,\}\).*$|\1|")
+%(echo APPLICATION_DIRECTORY | sed -e "s|\(^/[^/]\{1,\}\).*$|\1|")
 
 %post
-if [ "RUNTIME_INSTALLER" != "true" ]; then
-ADD_LAUNCHERS_INSTALL
-    xdg-desktop-menu install --novendor INSTALLATION_DIRECTORY/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
-FILE_ASSOCIATION_INSTALL
-fi
+DESKTOP_COMMANDS_INSTALL
 
 %preun
-if [ "RUNTIME_INSTALLER" != "true" ]; then
-ADD_LAUNCHERS_REMOVE
-    xdg-desktop-menu uninstall --novendor INSTALLATION_DIRECTORY/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
-FILE_ASSOCIATION_REMOVE
-fi
+UTILITY_SCRIPTS
+DESKTOP_COMMANDS_UNINSTALL
 
 %clean
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/utils.sh	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,104 @@
+#
+# Remove $1 desktop file from the list of default handlers for $2 mime type
+# in $3 file dumping output to stdout.
+#
+_filter_out_default_mime_handler ()
+{
+  local defaults_list="$3"
+
+  local desktop_file="$1"
+  local mime_type="$2"
+
+  awk -f- "$defaults_list" <<EOF
+  BEGIN {
+    mime_type="$mime_type"
+    mime_type_regexp="~" mime_type "="
+    desktop_file="$desktop_file"
+  }
+  \$0 ~ mime_type {
+    \$0 = substr(\$0, length(mime_type) + 2);
+    split(\$0, desktop_files, ";")
+    remaining_desktop_files
+    counter=0
+    for (idx in desktop_files) {
+      if (desktop_files[idx] != desktop_file) {
+        ++counter;
+      }
+    }
+    if (counter) {
+      printf mime_type "="
+      for (idx in desktop_files) {
+        if (desktop_files[idx] != desktop_file) {
+          printf desktop_files[idx]
+          if (--counter) {
+            printf ";"
+          }
+        }
+      }
+      printf "\n"
+    }
+    next
+  }
+
+  { print }
+EOF
+}
+
+
+#
+# Remove $2 desktop file from the list of default handlers for $@ mime types
+# in $1 file.
+# Result is saved in $1 file.
+#
+_uninstall_default_mime_handler ()
+{
+  local defaults_list=$1
+  shift
+  [ -f "$defaults_list" ] || return 0
+
+  local desktop_file="$1"
+  shift
+
+  tmpfile1=$(mktemp)
+  tmpfile2=$(mktemp)
+  cat "$defaults_list" > "$tmpfile1"
+
+  local v
+  local update=
+  for mime in "$@"; do
+    _filter_out_default_mime_handler "$desktop_file" "$mime" "$tmpfile1" > "$tmpfile2"
+    v="$tmpfile2"
+    tmpfile2="$tmpfile1"
+    tmpfile1="$v"
+
+    if ! diff -q "$tmpfile1" "$tmpfile2" > /dev/null; then
+      update=yes
+      trace Remove $desktop_file default handler for $mime mime type from $defaults_list file
+    fi
+  done
+
+  if [ -n "$update" ]; then
+    cat "$tmpfile1" > "$defaults_list"
+    trace "$defaults_list" file updated
+  fi
+
+  rm -f "$tmpfile1" "$tmpfile2"
+}
+
+
+#
+# Remove $1 desktop file from the list of default handlers for $@ mime types
+# in all known system defaults lists.
+#
+uninstall_default_mime_handler ()
+{
+  for f in /usr/share/applications/defaults.list /usr/local/share/applications/defaults.list; do
+    _uninstall_default_mime_handler "$f" "$@"
+  done
+}
+
+
+trace ()
+{
+  echo "$@"
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.jpackage.internal;
+
+import java.nio.file.Path;
+import java.util.Map;
+
+
+/**
+ * Application directory layout.
+ */
+final class ApplicationLayout implements PathGroup.Facade<ApplicationLayout> {
+    enum PathRole {
+        RUNTIME, APP, LAUNCHERS_DIR, DESKTOP
+    }
+
+    ApplicationLayout(Map<Object, Path> paths) {
+        data = new PathGroup(paths);
+    }
+
+    private ApplicationLayout(PathGroup data) {
+        this.data = data;
+    }
+
+    @Override
+    public PathGroup pathGroup() {
+        return data;
+    }
+
+    @Override
+    public ApplicationLayout resolveAt(Path root) {
+        return new ApplicationLayout(pathGroup().resolveAt(root));
+    }
+
+    /**
+     * Path to launchers directory.
+     */
+    Path launchersDirectory() {
+        return pathGroup().getPath(PathRole.LAUNCHERS_DIR);
+    }
+
+    /**
+     * Path to application data directory.
+     */
+    Path appDirectory() {
+        return pathGroup().getPath(PathRole.APP);
+    }
+
+    /**
+     * Path to Java runtime directory.
+     */
+    Path runtimeDirectory() {
+        return pathGroup().getPath(PathRole.RUNTIME);
+    }
+
+    /**
+     * Path to directory with application's desktop integration files.
+     */
+    Path destktopIntegrationDirectory() {
+        return pathGroup().getPath(PathRole.DESKTOP);
+    }
+
+    static ApplicationLayout unixApp() {
+        return new ApplicationLayout(Map.of(
+                PathRole.LAUNCHERS_DIR, Path.of("bin"),
+                PathRole.APP, Path.of("app"),
+                PathRole.RUNTIME, Path.of("runtime"),
+                PathRole.DESKTOP, Path.of("bin")
+        ));
+    }
+
+    static ApplicationLayout windowsApp() {
+        return new ApplicationLayout(Map.of(
+                PathRole.LAUNCHERS_DIR, Path.of(""),
+                PathRole.APP, Path.of("app"),
+                PathRole.RUNTIME, Path.of("runtime"),
+                PathRole.DESKTOP, Path.of("")
+        ));
+    }
+
+    static ApplicationLayout platformApp() {
+        if (Platform.getPlatform() == Platform.WINDOWS) {
+            return windowsApp();
+        }
+
+        return unixApp();
+    }
+
+    static ApplicationLayout javaRuntime() {
+        return new ApplicationLayout(Map.of(PathRole.RUNTIME, Path.of("")));
+    }
+
+    private final PathGroup data;
+}
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Arguments.java	Tue Sep 24 13:41:16 2019 -0400
@@ -525,7 +525,7 @@
                 Log.error(msg1);
                 if (e.getCause() != null && e.getCause() != e) {
                     String msg2 = e.getCause().getMessage();
-                    if (!msg1.contains(msg2)) {
+                    if (msg2 != null && !msg1.contains(msg2)) {
                         Log.error(msg2);
                     }
                 }
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java	Tue Sep 24 13:41:16 2019 -0400
@@ -79,21 +79,7 @@
     }
 
     public static void copyRecursive(Path src, Path dest) throws IOException {
-        Files.walkFileTree(src, new SimpleFileVisitor<Path>() {
-            @Override
-            public FileVisitResult preVisitDirectory(final Path dir,
-                    final BasicFileAttributes attrs) throws IOException {
-                Files.createDirectories(dest.resolve(src.relativize(dir)));
-                return FileVisitResult.CONTINUE;
-            }
-
-            @Override
-            public FileVisitResult visitFile(final Path file,
-                    final BasicFileAttributes attrs) throws IOException {
-                Files.copy(file, dest.resolve(src.relativize(file)));
-                return FileVisitResult.CONTINUE;
-            }
-        });
+        copyRecursive(src, dest, List.of());
     }
 
     public static void copyRecursive(Path src, Path dest,
@@ -148,23 +134,6 @@
         destFile.setReadable(true, false);
     }
 
-    public static long getFolderSize(File folder) {
-        long foldersize = 0;
-
-        File[] children = folder.listFiles();
-        if (children != null) {
-            for (File f : children) {
-                if (f.isDirectory()) {
-                    foldersize += getFolderSize(f);
-                } else {
-                    foldersize += f.length();
-                }
-            }
-        }
-
-        return foldersize;
-    }
-
     // run "launcher paramfile" in the directory where paramfile is kept
     public static void run(String launcher, File paramFile)
             throws IOException {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PathGroup.java	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.jpackage.internal;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+
+/**
+ * Group of paths.
+ * Each path in the group is assigned a unique id.
+ */
+final class PathGroup {
+    PathGroup(Map<Object, Path> paths) {
+        entries = Collections.unmodifiableMap(paths);
+    }
+
+    Path getPath(Object id) {
+        return entries.get(id);
+    }
+
+    /**
+     * All configured entries.
+     */
+    Collection<Path> paths() {
+        return entries.values();
+    }
+
+    /**
+     * Root entries.
+     */
+    List<Path> roots() {
+        // Sort by the number of path components in ascending order.
+        List<Path> sorted = paths().stream().sorted(
+                (a, b) -> a.getNameCount() - b.getNameCount()).collect(
+                        Collectors.toList());
+
+        return paths().stream().filter(
+                v -> v == sorted.stream().sequential().filter(
+                        v2 -> v == v2 || v2.endsWith(v)).findFirst().get()).collect(
+                        Collectors.toList());
+    }
+
+    long sizeInBytes() throws IOException {
+        long reply = 0;
+        for (Path dir : roots().stream().filter(f -> Files.isDirectory(f)).collect(
+                Collectors.toList())) {
+            reply += Files.walk(dir).filter(p -> Files.isRegularFile(p)).mapToLong(
+                    f -> f.toFile().length()).sum();
+        }
+        return reply;
+    }
+
+    PathGroup resolveAt(Path root) {
+        return new PathGroup(entries.entrySet().stream().collect(
+                Collectors.toMap(e -> e.getKey(),
+                        e -> root.resolve(e.getValue()))));
+    }
+
+    void copy(PathGroup dst) throws IOException {
+        copy(this, dst, false);
+    }
+
+    void move(PathGroup dst) throws IOException {
+        copy(this, dst, true);
+    }
+
+    static interface Facade<T> {
+        PathGroup pathGroup();
+
+        default Collection<Path> paths() {
+            return pathGroup().paths();
+        }
+
+        default List<Path> roots() {
+            return pathGroup().roots();
+        }
+
+        default long sizeInBytes() throws IOException {
+            return pathGroup().sizeInBytes();
+        }
+
+        T resolveAt(Path root);
+
+        default void copy(Facade<T> dst) throws IOException {
+            pathGroup().copy(dst.pathGroup());
+        }
+
+        default void move(Facade<T> dst) throws IOException {
+            pathGroup().move(dst.pathGroup());
+        }
+    }
+
+    private static void copy(PathGroup src, PathGroup dst, boolean move) throws
+            IOException {
+        copy(move, src.entries.keySet().stream().filter(
+                id -> dst.entries.containsKey(id)).map(id -> Map.entry(
+                src.entries.get(id), dst.entries.get(id))).collect(
+                Collectors.toCollection(ArrayList::new)));
+    }
+
+    private static void copy(boolean move, List<Map.Entry<Path, Path>> entries)
+            throws IOException {
+
+        // Reorder entries. Entries with source entries with the least amount of
+        // descending entries found between source entries should go first.
+        entries.sort((e1, e2) -> e1.getKey().getNameCount() - e2.getKey().getNameCount());
+
+        for (var entry : entries.stream().sequential().filter(e -> {
+            return e == entries.stream().sequential().filter(e2 -> isDuplicate(e2, e)).findFirst().get();
+                }).collect(Collectors.toList())) {
+            Path src = entry.getKey();
+            Path dst = entry.getValue();
+
+            if (src.equals(dst)) {
+                continue;
+            }
+
+            Files.createDirectories(dst.getParent());
+            if (move) {
+                Files.move(src, dst);
+            } else if (src.toFile().isDirectory()) {
+                IOUtils.copyRecursive(src, dst);
+            } else {
+                IOUtils.copyFile(src.toFile(), dst.toFile());
+            }
+        }
+    }
+
+    private static boolean isDuplicate(Map.Entry<Path, Path> a,
+            Map.Entry<Path, Path> b) {
+        if (a == b || a.equals(b)) {
+            return true;
+        }
+
+        if (b.getKey().getNameCount() < a.getKey().getNameCount()) {
+            return isDuplicate(b, a);
+        }
+
+        if (!a.getKey().endsWith(b.getKey())) {
+            return false;
+        }
+
+        Path relativeSrcPath = a.getKey().relativize(b.getKey());
+        Path relativeDstPath = a.getValue().relativize(b.getValue());
+
+        return relativeSrcPath.equals(relativeDstPath);
+    }
+
+    private final Map<Object, Path> entries;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PlatformPackage.java	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.jpackage.internal;
+
+import java.nio.file.Path;
+
+/**
+ *
+ * Platform package of an application.
+ */
+interface PlatformPackage {
+    /**
+     * Platform-specific package name.
+     */
+    String name();
+
+    /**
+     * Root directory where sources for packaging tool should be stored
+     */
+    Path sourceRoot();
+
+    /**
+     * Source application layout from which to build the package.
+     */
+    ApplicationLayout sourceApplicationLayout();
+
+    /**
+     * Application layout of the installed package.
+     */
+    ApplicationLayout installedApplicationLayout();
+}
--- a/test/jdk/tools/jpackage/apps/image/Hello.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/apps/image/Hello.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,60 +21,39 @@
  * questions.
  */
 
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.PrintWriter;
+import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.io.IOException;
 
 public class Hello {
 
     private static final String MSG = "jpackage test application";
     private static final int EXPECTED_NUM_OF_PARAMS = 3; // Starts at 1
 
-    public static void main(String[] args) {
-        printToStdout(args);
-        printToFile(args);
+    public static void main(String[] args) throws IOException {
+        printArgs(args, System.out);
+
+        try (PrintStream out = new PrintStream(new BufferedOutputStream(
+                new FileOutputStream("appOutput.txt")))) {
+            printArgs(args, out);
+        }
     }
 
-    private static void printToStdout(String[] args) {
-        System.out.println(MSG);
+    private static void printArgs(String[] args, PrintStream out) {
+        out.println(MSG);
 
-        System.out.println("args.length: " + args.length);
+        out.println("args.length: " + args.length);
 
         for (String arg : args) {
-            System.out.println(arg);
+            out.println(arg);
         }
 
         for (int index = 1; index <= EXPECTED_NUM_OF_PARAMS; index++) {
             String value = System.getProperty("param" + index);
             if (value != null) {
-                System.out.println("-Dparam" + index + "=" + value);
+                out.println("-Dparam" + index + "=" + value);
             }
         }
     }
-
-    private static void printToFile(String[] args) {
-        String outputFile = "appOutput.txt";
-        File file = new File(outputFile);
-
-        try (PrintWriter out
-                = new PrintWriter(new BufferedWriter(new FileWriter(file)))) {
-            out.println(MSG);
-
-            out.println("args.length: " + args.length);
-
-            for (String arg : args) {
-                out.println(arg);
-            }
-
-            for (int index = 1; index <= EXPECTED_NUM_OF_PARAMS; index++) {
-                String value = System.getProperty("param" + index);
-                if (value != null) {
-                    out.println("-Dparam" + index + "=" + value);
-                }
-            }
-        } catch (Exception ex) {
-            System.err.println(ex.getMessage());
-        }
-    }
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java	Tue Sep 24 13:41:16 2019 -0400
@@ -31,7 +31,10 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.spi.ToolProvider;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 public final class Executor extends CommandArguments<Executor> {
@@ -75,6 +78,11 @@
         return this;
     }
 
+    public Executor dumpOtput() {
+        saveOutputType = SaveOutputType.DUMP;
+        return this;
+    }
+
     public class Result {
 
         Result(int exitCode) {
@@ -82,7 +90,7 @@
         }
 
         public String getFirstLineOfOutput() {
-            return output.get(0).trim();
+            return output.get(0);
         }
 
         public List<String> getOutput() {
@@ -126,6 +134,14 @@
         throw new IllegalStateException("No command to execute");
     }
 
+    public String executeAndGetFirstLineOfOutput() {
+        return saveFirstLineOfOutput().execute().assertExitCodeIsZero().getFirstLineOfOutput();
+    }
+
+    public List<String> executeAndGetOutput() {
+        return saveOutput().execute().assertExitCodeIsZero().getOutput();
+    }
+
     private Result runExecutable() throws IOException, InterruptedException {
         List<String> command = new ArrayList<>();
         command.add(executable);
@@ -134,10 +150,16 @@
         ProcessBuilder builder = new ProcessBuilder(command);
         StringBuilder sb = new StringBuilder(getPrintableCommandLine());
         if (saveOutputType != SaveOutputType.NONE) {
-            outputFile = Test.createTempFile(".out");
             builder.redirectErrorStream(true);
-            builder.redirectOutput(outputFile.toFile());
-            sb.append(String.format("; redirect output to [%s]", outputFile));
+
+            if (saveOutputType == SaveOutputType.DUMP) {
+                builder.inheritIO();
+                sb.append("; redirect output to stdout");
+            } else {
+                outputFile = Test.createTempFile(".out");
+                builder.redirectOutput(outputFile.toFile());
+                sb.append(String.format("; redirect output to [%s]", outputFile));
+            }
         }
         if (directory != null) {
             builder.directory(directory.toFile());
@@ -150,8 +172,10 @@
             Result reply = new Result(process.waitFor());
             Test.trace("Done. Exit code: " + reply.exitCode);
             if (saveOutputType == SaveOutputType.FIRST_LINE) {
+                // If the command produced no ouput, save null in 'result.output' list.
                 reply.output = Arrays.asList(
-                        Files.readAllLines(outputFile).stream().findFirst().get());
+                        Files.readAllLines(outputFile).stream().findFirst().orElse(
+                                null));
             } else if (saveOutputType == SaveOutputType.FULL) {
                 reply.output = Collections.unmodifiableList(Files.readAllLines(
                         outputFile));
@@ -184,18 +208,28 @@
     }
 
     public String getPrintableCommandLine() {
-        String argsStr = String.format("; args(%d)=%s", args.size(),
-                Arrays.toString(args.toArray()));
-
+        final String exec;
+        String format = "[%s](%d)";
         if (toolProvider == null && executable == null) {
-            return "[null]; " + argsStr;
+            exec = "<null>";
+        } else if (toolProvider != null) {
+            format = "tool provider " + format;
+            exec = toolProvider.name();
+        } else {
+            exec = executable;
         }
 
-        if (toolProvider != null) {
-            return String.format("tool provider=[%s]; ", toolProvider.name()) + argsStr;
-        }
+        return String.format(format, printCommandLine(exec, args),
+                args.size() + 1);
+    }
 
-        return String.format("[%s]; ", executable) + argsStr;
+    private static String printCommandLine(String executable, List<String> args) {
+        // Want command line printed in a way it can be easily copy/pasted
+        // to be executed manally
+        Pattern regex = Pattern.compile("\\s");
+        return Stream.concat(Stream.of(executable), args.stream()).map(
+                v -> (v.isEmpty() || regex.matcher(v).find()) ? "\"" + v + "\"" : v).collect(
+                        Collectors.joining(" "));
     }
 
     private ToolProvider toolProvider;
@@ -204,6 +238,6 @@
     private Path directory;
 
     private static enum SaveOutputType {
-        NONE, FULL, FIRST_LINE
+        NONE, FULL, FIRST_LINE, DUMP
     };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.jpackage.test;
+
+import java.nio.file.Path;
+import java.util.Map;
+
+
+public class FileAssociations {
+    public FileAssociations(String faSuffixName) {
+        suffixName = faSuffixName;
+        setFilename("fa");
+        setDescription("jpackage test extention");
+    }
+
+    public void createFile() {
+        Test.createPropertiesFile(file,
+                Map.entry("extension", suffixName),
+                Map.entry("mime-type", getMime()),
+                Map.entry("description", description));
+    }
+
+    final public FileAssociations setFilename(String v) {
+        file = Test.workDir().resolve(v + ".properties");
+        return this;
+    }
+
+    final public FileAssociations setDescription(String v) {
+        description = v;
+        return this;
+    }
+
+    public Path getPropertiesFile() {
+        return file;
+    }
+
+    public String getSuffix() {
+        return "." + suffixName;
+    }
+
+    public String getMime() {
+        return "application/x-jpackage-" + suffixName;
+    }
+
+    private Path file;
+    final private String suffixName;
+    private String description;
+}
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java	Tue Sep 24 13:41:16 2019 -0400
@@ -30,31 +30,36 @@
 import java.util.Enumeration;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
-
+import java.util.function.Consumer;
 
 public class HelloApp {
+
+    private static final String MAIN_CLASS = "Hello";
+    private static final String JAR_FILENAME = "hello.jar";
+    private static final Consumer<JPackageCommand> CREATE_JAR_ACTION = (cmd) -> {
+        new JarBuilder()
+                .setOutputJar(cmd.inputDir().resolve(JAR_FILENAME).toFile())
+                .setMainClass(MAIN_CLASS)
+                .addSourceFile(Test.TEST_SRC_ROOT.resolve(
+                        Path.of("apps", "image", MAIN_CLASS + ".java")))
+                .create();
+    };
+
     static void addTo(JPackageCommand cmd) {
-        cmd.addAction(new Runnable() {
-            @Override
-            public void run() {
-                String mainClass = "Hello";
-                Path jar = cmd.inputDir().resolve("hello.jar");
-                new JarBuilder()
-                        .setOutputJar(jar.toFile())
-                        .setMainClass(mainClass)
-                        .addSourceFile(Test.TEST_SRC_ROOT.resolve(
-                                Path.of("apps", "image", mainClass + ".java")))
-                        .create();
-                cmd.addArguments("--main-jar", jar.getFileName().toString());
-                cmd.addArguments("--main-class", mainClass);
-            }
-        });
+        cmd.addAction(CREATE_JAR_ACTION);
+        cmd.addArguments("--main-jar", JAR_FILENAME);
+        cmd.addArguments("--main-class", MAIN_CLASS);
         if (PackageType.WINDOWS.contains(cmd.packageType())) {
             cmd.addArguments("--win-console");
         }
     }
 
-    public static void verifyOutputFile(Path outputFile, String ... args) {
+    static void verifyOutputFile(Path outputFile, String... args) {
+        if (!outputFile.isAbsolute()) {
+            verifyOutputFile(outputFile.toAbsolutePath().normalize(), args);
+            return;
+        }
+
         Test.assertFileExists(outputFile, true);
 
         List<String> output = null;
@@ -87,9 +92,35 @@
                 counter.incrementAndGet(), outputFile)));
     }
 
+    public static void executeLauncherAndVerifyOutput(JPackageCommand cmd) {
+        final Path launcherPath;
+        if (cmd.packageType() == PackageType.IMAGE) {
+            launcherPath = cmd.appImage().resolve(cmd.launcherPathInAppImage());
+            if (cmd.isFakeRuntimeInAppImage(String.format(
+                    "Not running [%s] launcher from application image",
+                    launcherPath))) {
+                return;
+            }
+        } else {
+            launcherPath = cmd.launcherInstallationPath();
+            if (cmd.isFakeRuntimeInstalled(String.format(
+                    "Not running [%s] launcher", launcherPath))) {
+                return;
+            }
+        }
+
+        executeAndVerifyOutput(launcherPath, cmd.getAllArgumentValues(
+                "--arguments"));
+    }
+
     public static void executeAndVerifyOutput(Path helloAppLauncher,
             String... defaultLauncherArgs) {
         File outputFile = Test.workDir().resolve(OUTPUT_FILENAME).toFile();
+        try {
+            Files.deleteIfExists(outputFile.toPath());
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
         new Executor()
                 .setDirectory(outputFile.getParentFile().toPath())
                 .setExecutable(helloAppLauncher.toString())
@@ -99,5 +130,5 @@
         verifyOutputFile(outputFile.toPath(), defaultLauncherArgs);
     }
 
-    public final static String OUTPUT_FILENAME = "appOutput.txt";
+    final static String OUTPUT_FILENAME = "appOutput.txt";
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java	Tue Sep 24 13:41:16 2019 -0400
@@ -22,7 +22,11 @@
  */
 package jdk.jpackage.test;
 
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -30,9 +34,9 @@
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
-import java.util.stream.Stream;
 
 /**
  * jpackage command line with prerequisite actions. Prerequisite actions can be
@@ -45,14 +49,14 @@
         actions = new ArrayList<>();
     }
 
-    static JPackageCommand createImmutable(JPackageCommand v) {
+    JPackageCommand createImmutableCopy() {
         JPackageCommand reply = new JPackageCommand();
         reply.immutable = true;
-        reply.args.addAll(v.args);
+        reply.args.addAll(args);
         return reply;
     }
 
-    public void setArgumentValue(String argName, String newValue) {
+    public JPackageCommand setArgumentValue(String argName, String newValue) {
         verifyMutable();
 
         String prevArg = null;
@@ -67,7 +71,7 @@
                     it.previous();
                     it.remove();
                 }
-                return;
+                return this;
             }
             prevArg = value;
         }
@@ -75,6 +79,16 @@
         if (newValue != null) {
             addArguments(argName, newValue);
         }
+
+        return this;
+    }
+
+    public JPackageCommand setArgumentValue(String argName, Path newValue) {
+        return setArgumentValue(argName, newValue.toString());
+    }
+
+    public JPackageCommand removeArgument(String argName) {
+        return setArgumentValue(argName, (String)null);
     }
 
     public boolean hasArgument(String argName) {
@@ -130,6 +144,10 @@
         return values.toArray(String[]::new);
     }
 
+    public JPackageCommand addArguments(String name, Path value) {
+        return addArguments(name, value.toString());
+    }
+
     public PackageType packageType() {
         return getArgumentValue("--package-type",
                 () -> PackageType.DEFAULT,
@@ -153,17 +171,54 @@
     }
 
     public boolean isRuntime() {
-        return getArgumentValue("--runtime-image", () -> false, v -> true);
+        return  hasArgument("--runtime-image")
+                && !hasArgument("--main-jar")
+                && !hasArgument("--module")
+                && !hasArgument("--app-image");
     }
 
     public JPackageCommand setDefaultInputOutput() {
-        verifyMutable();
-        addArguments("--input", Test.defaultInputDir().toString());
-        addArguments("--dest", Test.defaultOutputDir().toString());
+        addArguments("--input", Test.defaultInputDir());
+        addArguments("--dest", Test.defaultOutputDir());
         return this;
     }
 
-    JPackageCommand addAction(Runnable action) {
+    public JPackageCommand setFakeRuntime() {
+        verifyMutable();
+
+        try {
+            Path fakeRuntimeDir = Test.workDir().resolve("fake_runtime");
+            Files.createDirectories(fakeRuntimeDir);
+
+            if (Test.isWindows() || Test.isLinux()) {
+                // Needed to make WindowsAppBundler happy as it copies MSVC dlls
+                // from `bin` directory.
+                // Need to make the code in rpm spec happy as it assumes there is
+                // always something in application image.
+                fakeRuntimeDir.resolve("bin").toFile().mkdir();
+            }
+
+            Path bulk = fakeRuntimeDir.resolve(Path.of("bin", "bulk"));
+
+            // Mak sure fake runtime takes some disk space.
+            // Package bundles with 0KB size are unexpected and considered
+            // an error by PackageTest.
+            Files.createDirectories(bulk.getParent());
+            try (FileOutputStream out = new FileOutputStream(bulk.toFile())) {
+                byte[] bytes = new byte[4 * 1024];
+                new SecureRandom().nextBytes(bytes);
+                out.write(bytes);
+            }
+
+            addArguments("--runtime-image", fakeRuntimeDir);
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
+
+        return this;
+    }
+
+    JPackageCommand addAction(Consumer<JPackageCommand> action) {
         verifyMutable();
         actions.add(action);
         return this;
@@ -184,15 +239,7 @@
     }
 
     JPackageCommand setDefaultAppName() {
-        StackTraceElement st[] = Thread.currentThread().getStackTrace();
-        for (StackTraceElement ste : st) {
-            if ("main".equals(ste.getMethodName())) {
-                String name = ste.getClassName();
-                name = Stream.of(name.split("[.$]")).reduce((f, l) -> l).get();
-                addArguments("--name", name);
-                break;
-            }
-        }
+        addArguments("--name", Test.enclosingMainMethodClass().getSimpleName());
         return this;
     }
 
@@ -227,6 +274,12 @@
         }
 
         if (PackageType.LINUX.contains(type)) {
+            if (isRuntime()) {
+                // Not fancy, but OK.
+                return Path.of(getArgumentValue("--install-dir", () -> "/opt"),
+                        LinuxHelper.getPackageName(this));
+            }
+
             // Launcher is in "bin" subfolder of the installation directory.
             return launcherInstallationPath().getParent().getParent();
         }
@@ -243,6 +296,21 @@
     }
 
     /**
+     * Returns path where application's Java runtime will be installed.
+     * If the command will package Java run-time only, still returns path to
+     * runtime subdirectory.
+     *
+     * E.g. on Linux for app named `Foo` the function will return
+     * `/opt/foo/runtime`
+     */
+    public Path appRuntimeInstallationDirectory() {
+        if (PackageType.IMAGE == packageType()) {
+            return null;
+        }
+        return appInstallationDirectory().resolve("runtime");
+    }
+
+    /**
      * Returns path where application launcher will be installed.
      * If the command will package Java run-time only, still returns path to
      * application launcher.
@@ -317,17 +385,81 @@
         throw new IllegalArgumentException("Unexpected package type");
     }
 
+    /**
+     * Returns path to runtime directory relative to image directory.
+     *
+     * Function will always return "runtime".
+     *
+     * @throws IllegalArgumentException if command is configured for platform
+     * packaging
+     */
+    public Path appRuntimeDirectoryInAppImage() {
+        final PackageType type = packageType();
+        if (PackageType.IMAGE != type) {
+            throw new IllegalArgumentException("Unexpected package type");
+        }
+
+        return Path.of("runtime");
+    }
+
+    public boolean isFakeRuntimeInAppImage(String msg) {
+        return isFakeRuntime(appImage().resolve(
+                appRuntimeDirectoryInAppImage()), msg);
+    }
+
+    public boolean isFakeRuntimeInstalled(String msg) {
+        return isFakeRuntime(appRuntimeInstallationDirectory(), msg);
+    }
+
+    private static boolean isFakeRuntime(Path runtimeDir, String msg) {
+        final List<Path> criticalRuntimeFiles;
+        if (Test.isWindows()) {
+            criticalRuntimeFiles = List.of(Path.of("server\\jvm.dll"));
+        } else if (Test.isLinux()) {
+            criticalRuntimeFiles = List.of(Path.of("server/libjvm.so"));
+        } else if (Test.isOSX()) {
+            criticalRuntimeFiles = List.of(Path.of("server/libjvm.dylib"));
+        } else {
+            throw new IllegalArgumentException("Unknwon platform");
+        }
+
+        if (criticalRuntimeFiles.stream().filter(v -> v.toFile().exists())
+                .findFirst().orElse(null) == null) {
+            // Fake runtime
+            Test.trace(String.format(
+                    "%s because application runtime directory [%s] is incomplete",
+                    msg, runtimeDir));
+            return true;
+        }
+        return false;
+    }
+
     public Executor.Result execute() {
         verifyMutable();
         if (actions != null) {
-            actions.stream().forEach(r -> r.run());
+            actions.stream().forEach(r -> r.accept(this));
         }
+
         return new Executor()
                 .setExecutable(JavaTool.JPACKAGE)
-                .addArguments(args)
+                .dumpOtput()
+                .addArguments(new JPackageCommand().addArguments(
+                                args).adjustArgumentsBeforeExecution().args)
                 .execute();
     }
 
+    private JPackageCommand adjustArgumentsBeforeExecution() {
+        if (!hasArgument("--runtime-image") && !hasArgument("--app-image") && DEFAULT_RUNTIME_IMAGE != null) {
+            addArguments("--runtime-image", DEFAULT_RUNTIME_IMAGE);
+        }
+
+        if (!hasArgument("--verbose") && Test.VERBOSE_JPACKAGE) {
+            addArgument("--verbose");
+        }
+
+        return this;
+    }
+
     String getPrintableCommandLine() {
         return new Executor()
                 .setExecutable(JavaTool.JPACKAGE)
@@ -350,7 +482,7 @@
         return !immutable;
     }
 
-    private final List<Runnable> actions;
+    private final List<Consumer<JPackageCommand>> actions;
     private boolean immutable;
 
     private final static Map<String, PackageType> PACKAGE_TYPES
@@ -364,4 +496,20 @@
                     return reply;
                 }
             }.get();
+
+    public final static Path DEFAULT_RUNTIME_IMAGE;
+
+    static {
+        // Set the property to the path of run-time image to speed up
+        // building app images and platform bundles by avoiding running jlink
+        // The value of the property will be automativcally appended to
+        // jpackage command line if the command line doesn't have
+        // `--runtime-image` parameter set.
+        String val = Test.getConfigProperty("runtime-image");
+        if (val != null) {
+            DEFAULT_RUNTIME_IMAGE = Path.of(val);
+        } else {
+            DEFAULT_RUNTIME_IMAGE = null;
+        }
+    }
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java	Tue Sep 24 13:41:16 2019 -0400
@@ -22,9 +22,15 @@
  */
 package jdk.jpackage.test;
 
+import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
 import java.util.stream.Stream;
 
 public class LinuxHelper {
@@ -68,7 +74,6 @@
         final Path packageFile = cmd.outputBundle();
 
         Executor exec = new Executor();
-        exec.saveOutput();
         switch (packageType) {
             case LINUX_DEB:
                 exec.setExecutable("dpkg")
@@ -83,7 +88,7 @@
                 break;
         }
 
-        Stream<String> lines = exec.execute().assertExitCodeIsZero().getOutput().stream();
+        Stream<String> lines = exec.executeAndGetOutput().stream();
         if (packageType == PackageType.LINUX_DEB) {
             // Typical text lines produced by dpkg look like:
             // drwxr-xr-x root/root         0 2019-08-30 05:30 ./opt/appcategorytest/runtime/lib/
@@ -109,26 +114,176 @@
         }).get();
     }
 
+    static long getInstalledPackageSizeKB(JPackageCommand cmd) {
+        cmd.verifyIsOfType(PackageType.LINUX);
+
+        final Path packageFile = cmd.outputBundle();
+        switch (cmd.packageType()) {
+            case LINUX_DEB:
+                return Long.parseLong(getDebBundleProperty(packageFile,
+                        "Installed-Size"));
+
+            case LINUX_RPM:
+                return Long.parseLong(getRpmBundleProperty(packageFile, "Size")) >> 10;
+        }
+
+        return 0;
+    }
+
     static String getDebBundleProperty(Path bundle, String fieldName) {
         return new Executor()
-                .saveFirstLineOfOutput()
                 .setExecutable("dpkg-deb")
                 .addArguments("-f", bundle.toString(), fieldName)
-                .execute()
-                .assertExitCodeIsZero().getFirstLineOfOutput();
+                .executeAndGetFirstLineOfOutput();
     }
 
-    static String geRpmBundleProperty(Path bundle, String fieldName) {
+    static String getRpmBundleProperty(Path bundle, String fieldName) {
         return new Executor()
-                .saveFirstLineOfOutput()
                 .setExecutable("rpm")
                 .addArguments(
                         "-qp",
                         "--queryformat",
                         String.format("%%{%s}", fieldName),
                         bundle.toString())
-                .execute()
-                .assertExitCodeIsZero().getFirstLineOfOutput();
+                .executeAndGetFirstLineOfOutput();
+    }
+
+    static void addDebBundleDesktopIntegrationVerifier(PackageTest test,
+            boolean integrated) {
+        Function<List<String>, String> verifier = (lines) -> {
+            // Lookup for xdg commands
+            return lines.stream().filter(line -> {
+                Set<String> words = Set.of(line.split("\\s+"));
+                return words.contains("xdg-desktop-menu") || words.contains(
+                        "xdg-mime") || words.contains("xdg-icon-resource");
+            }).findFirst().orElse(null);
+        };
+
+        test.addBundleVerifier(cmd -> {
+            Test.withTempDirectory(tempDir -> {
+                try {
+                    // Extract control Debian package files into temporary directory
+                    new Executor()
+                    .setExecutable("dpkg")
+                    .addArguments(
+                            "-e",
+                            cmd.outputBundle().toString(),
+                            tempDir.toString()
+                    ).execute().assertExitCodeIsZero();
+
+                    Path controlFile = Path.of("postinst");
+
+                    // Lookup for xdg commands in postinstall script
+                    String lineWithXsdCommand = verifier.apply(
+                            Files.readAllLines(tempDir.resolve(controlFile)));
+                    String assertMsg = String.format(
+                            "Check if %s@%s control file uses xdg commands",
+                            cmd.outputBundle(), controlFile);
+                    if (integrated) {
+                        Test.assertNotNull(lineWithXsdCommand, assertMsg);
+                    } else {
+                        Test.assertNull(lineWithXsdCommand, assertMsg);
+                    }
+                } catch (IOException ex) {
+                    throw new RuntimeException(ex);
+                }
+            });
+        });
+    }
+
+    static void initFileAssociationsTestFile(Path testFile) {
+        try {
+            // Write something in test file.
+            // On Ubuntu and Oracle Linux empty files are considered
+            // plain text. Seems like a system bug.
+            //
+            // $ >foo.jptest1
+            // $ xdg-mime query filetype foo.jptest1
+            // text/plain
+            // $ echo > foo.jptest1
+            // $ xdg-mime query filetype foo.jptest1
+            // application/x-jpackage-jptest1
+            //
+            Files.write(testFile, Arrays.asList(""));
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private static Path getSystemDesktopFilesFolder() {
+        return Stream.of("/usr/share/applications",
+                "/usr/local/share/applications").map(Path::of).filter(dir -> {
+            return Files.exists(dir.resolve("defaults.list"));
+        }).findFirst().orElseThrow(() -> new RuntimeException(
+                "Failed to locate system .desktop files folder"));
+    }
+
+    static void addFileAssociationsVerifier(PackageTest test, FileAssociations fa) {
+        test.addInstallVerifier(cmd -> {
+            Test.withTempFile(fa.getSuffix(), testFile -> {
+                initFileAssociationsTestFile(testFile);
+
+                String mimeType = queryFileMimeType(testFile);
+
+                Test.assertEquals(fa.getMime(), mimeType, String.format(
+                        "Check mime type of [%s] file", testFile));
+
+                String desktopFileName = queryMimeTypeDefaultHandler(mimeType);
+
+                Path desktopFile = getSystemDesktopFilesFolder().resolve(
+                        desktopFileName);
+
+                Test.assertFileExists(desktopFile, true);
+
+                Test.trace(String.format("Reading [%s] file...", desktopFile));
+                String mimeHandler = null;
+                try {
+                    mimeHandler = Files.readAllLines(desktopFile).stream().peek(
+                            v -> Test.trace(v)).filter(
+                                    v -> v.startsWith("Exec=")).map(
+                                    v -> v.split("=", 2)[1]).findFirst().orElseThrow();
+                } catch (IOException ex) {
+                    throw new RuntimeException(ex);
+                }
+                Test.trace(String.format("Done"));
+
+                Test.assertEquals(cmd.launcherInstallationPath().toString(),
+                        mimeHandler, String.format(
+                                "Check mime type handler is the main application launcher"));
+
+            });
+        });
+
+        test.addUninstallVerifier(cmd -> {
+            Test.withTempFile(fa.getSuffix(), testFile -> {
+                initFileAssociationsTestFile(testFile);
+
+                String mimeType = queryFileMimeType(testFile);
+
+                Test.assertNotEquals(fa.getMime(), mimeType, String.format(
+                        "Check mime type of [%s] file", testFile));
+
+                String desktopFileName = queryMimeTypeDefaultHandler(fa.getMime());
+
+                Test.assertNull(desktopFileName, String.format(
+                        "Check there is no default handler for [%s] mime type",
+                        fa.getMime()));
+            });
+        });
+    }
+
+    private static String queryFileMimeType(Path file) {
+        return new Executor()
+                .setExecutable("xdg-mime")
+                .addArguments("query", "filetype", file.toString())
+                .executeAndGetFirstLineOfOutput();
+    }
+
+    private static String queryMimeTypeDefaultHandler(String mimeType) {
+        return new Executor()
+                .setExecutable("xdg-mime")
+                .addArguments("query", "default", mimeType)
+                .executeAndGetFirstLineOfOutput();
     }
 
     private static String getPackageArch(PackageType type) {
@@ -139,7 +294,6 @@
         String arch = archs.get(type);
         if (arch == null) {
             Executor exec = new Executor();
-            exec.saveFirstLineOfOutput();
             switch (type) {
                 case LINUX_DEB:
                     exec.setExecutable("dpkg").addArgument(
@@ -151,7 +305,7 @@
                             "--eval=%{_target_cpu}");
                     break;
             }
-            arch = exec.execute().assertExitCodeIsZero().getFirstLineOfOutput();
+            arch = exec.executeAndGetFirstLineOfOutput();
             archs.put(type, arch);
         }
         return arch;
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -22,19 +22,26 @@
  */
 package jdk.jpackage.test;
 
+import java.awt.Desktop;
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import static jdk.jpackage.test.PackageType.LINUX_DEB;
+import static jdk.jpackage.test.PackageType.LINUX_RPM;
 
 /**
  * Instance of PackageTest is for configuring and running a single jpackage
@@ -59,6 +66,7 @@
         forTypes();
         setJPackageExitCode(0);
         handlers = new HashMap<>();
+        namedInitializers = new HashSet<>();
         currentTypes.forEach(v -> handlers.put(v, new Handler(v)));
     }
 
@@ -79,14 +87,25 @@
     }
 
     public PackageTest setJPackageExitCode(int v) {
-        expectedJPackageExitCode = 0;
+        expectedJPackageExitCode = v;
+        return this;
+    }
+
+    private PackageTest addInitializer(Consumer<JPackageCommand> v, String id) {
+        if (id != null) {
+            if (namedInitializers.contains(id)) {
+                return this;
+            }
+
+            namedInitializers.add(id);
+        }
+        currentTypes.stream().forEach(type -> handlers.get(type).addInitializer(
+                v));
         return this;
     }
 
     public PackageTest addInitializer(Consumer<JPackageCommand> v) {
-        currentTypes.stream().forEach(type -> handlers.get(type).addInitializer(
-                v));
-        return this;
+        return addInitializer(v, null);
     }
 
     public PackageTest addBundleVerifier(
@@ -111,7 +130,7 @@
                     break;
 
                 case LINUX_RPM:
-                    propertyValue = LinuxHelper.geRpmBundleProperty(
+                    propertyValue = LinuxHelper.getRpmBundleProperty(
                             cmd.outputBundle(), propertyName);
                     break;
 
@@ -131,6 +150,13 @@
         });
     }
 
+    public PackageTest addBundleDesktopIntegrationVerifier(boolean integrated) {
+        forTypes(LINUX_DEB, () -> {
+            LinuxHelper.addDebBundleDesktopIntegrationVerifier(this, integrated);
+        });
+        return this;
+    }
+
     public PackageTest addInstallVerifier(Consumer<JPackageCommand> v) {
         currentTypes.stream().forEach(
                 type -> handlers.get(type).addInstallVerifier(v));
@@ -143,11 +169,70 @@
         return this;
     }
 
+    public PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa,
+            String... faLauncherDefaultArgs) {
+
+        addInitializer(cmd -> HelloApp.addTo(cmd), "HelloApp");
+        addInstallVerifier(cmd -> {
+            if (cmd.isFakeRuntimeInstalled(
+                    "Not running file associations test")) {
+                return;
+            }
+
+            Test.withTempFile(fa.getSuffix(), testFile -> {
+                if (PackageType.LINUX.contains(cmd.packageType())) {
+                    LinuxHelper.initFileAssociationsTestFile(testFile);
+                }
+
+                try {
+                    final Path appOutput = Path.of(HelloApp.OUTPUT_FILENAME);
+                    Files.deleteIfExists(appOutput);
+
+                    Test.trace(String.format("Use desktop to open [%s] file",
+                            testFile));
+                    Desktop.getDesktop().open(testFile.toFile());
+                    Test.waitForFileCreated(appOutput, 7);
+
+                    List<String> expectedArgs = new ArrayList<>(List.of(
+                            faLauncherDefaultArgs));
+                    expectedArgs.add(testFile.toString());
+
+                    // Wait a little bit after file has been created to
+                    // make sure there are no pending writes into it.
+                    Thread.sleep(3000);
+                    HelloApp.verifyOutputFile(appOutput, expectedArgs.toArray(
+                            String[]::new));
+                } catch (IOException | InterruptedException ex) {
+                    throw new RuntimeException(ex);
+                }
+            });
+        });
+
+        forTypes(PackageType.LINUX, () -> {
+            LinuxHelper.addFileAssociationsVerifier(this, fa);
+        });
+
+        return this;
+    }
+
+    private void forTypes(Collection<PackageType> types, Runnable action) {
+        Set<PackageType> oldTypes = Set.of(currentTypes.toArray(
+                PackageType[]::new));
+        try {
+            forTypes(types);
+            action.run();
+        } finally {
+            forTypes(oldTypes);
+        }
+    }
+
+    private void forTypes(PackageType type, Runnable action) {
+        forTypes(List.of(type), action);
+    }
+
     public PackageTest configureHelloApp() {
-        addInitializer(cmd -> HelloApp.addTo(cmd));
-        addInstallVerifier(cmd -> HelloApp.executeAndVerifyOutput(
-                cmd.launcherInstallationPath(), cmd.getAllArgumentValues(
-                "--arguments")));
+        addInitializer(cmd -> HelloApp.addTo(cmd), "HelloApp");
+        addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput);
         return this;
     }
 
@@ -230,23 +315,27 @@
                     result.assertExitCodeIs(expectedJPackageExitCode);
                     Test.assertFileExists(cmd.outputBundle(),
                             expectedJPackageExitCode == 0);
-                    verifyPackageBundle(JPackageCommand.createImmutable(cmd),
-                            result);
+                    verifyPackageBundle(cmd.createImmutableCopy(), result);
                     break;
 
-                case VERIFY_INSTALLED:
-                    verifyPackageInstalled(JPackageCommand.createImmutable(cmd));
+                case VERIFY_INSTALL:
+                    verifyPackageInstalled(cmd.createImmutableCopy());
                     break;
 
-                case VERIFY_UNINSTALLED:
-                    verifyPackageUninstalled(
-                            JPackageCommand.createImmutable(cmd));
+                case VERIFY_UNINSTALL:
+                    verifyPackageUninstalled(cmd.createImmutableCopy());
                     break;
             }
         }
 
         private void verifyPackageBundle(JPackageCommand cmd,
                 Executor.Result result) {
+            if (PackageType.LINUX.contains(cmd.packageType())) {
+                Test.assertNotEquals(0L, LinuxHelper.getInstalledPackageSizeKB(
+                        cmd), String.format(
+                                "Check installed size of [%s] package in KB is not zero",
+                                LinuxHelper.getPackageName(cmd)));
+            }
             bundleVerifiers.stream().forEach(v -> v.accept(cmd, result));
         }
 
@@ -255,14 +344,14 @@
                     cmd.getPrintableCommandLine()));
             if (cmd.isRuntime()) {
                 Test.assertDirectoryExists(
-                        cmd.appInstallationDirectory().resolve("runtime"), false);
+                        cmd.appRuntimeInstallationDirectory(), false);
                 Test.assertDirectoryExists(
                         cmd.appInstallationDirectory().resolve("app"), false);
+            } else {
+                Test.assertExecutableFileExists(cmd.launcherInstallationPath(),
+                        true);
             }
 
-            Test.assertExecutableFileExists(cmd.launcherInstallationPath(),
-                    !cmd.isRuntime());
-
             if (PackageType.WINDOWS.contains(cmd.packageType())) {
                 new WindowsHelper.AppVerifier(cmd);
             }
@@ -295,6 +384,7 @@
     private Collection<PackageType> currentTypes;
     private int expectedJPackageExitCode;
     private Map<PackageType, Handler> handlers;
+    private Set<String> namedInitializers;
     private Action action;
 
     /**
@@ -308,40 +398,45 @@
         /**
          * Verify bundle installed.
          */
-        VERIFY_INSTALLED,
+        VERIFY_INSTALL,
         /**
          * Verify bundle uninstalled.
          */
-        VERIFY_UNINSTALLED
+        VERIFY_UNINSTALL;
+
+        @Override
+        public String toString() {
+            return name().toLowerCase().replace('_', '-');
+        }
     };
     private final static Action DEFAULT_ACTION;
     private final static File bundleOutputDir;
 
     static {
-        final String JPACKAGE_TEST_OUTPUT = "jpackage.test.output";
-
-        String val = System.getProperty(JPACKAGE_TEST_OUTPUT);
+        final String propertyName = "output";
+        String val = Test.getConfigProperty(propertyName);
         if (val == null) {
             bundleOutputDir = null;
         } else {
             bundleOutputDir = new File(val).getAbsoluteFile();
 
-            Test.assertTrue(bundleOutputDir.isDirectory(), String.format(
-                    "Check value of %s property [%s] references a directory",
-                    JPACKAGE_TEST_OUTPUT, bundleOutputDir));
-            Test.assertTrue(bundleOutputDir.canWrite(), String.format(
-                    "Check value of %s property [%s] references writable directory",
-                    JPACKAGE_TEST_OUTPUT, bundleOutputDir));
+            if (!bundleOutputDir.isDirectory()) {
+                throw new IllegalArgumentException(String.format(
+                        "Invalid value of %s sytem property: [%s]. Should be existing directory",
+                        Test.getConfigPropertyName(propertyName),
+                        bundleOutputDir));
+            }
         }
     }
 
     static {
-        if (System.getProperty("jpackage.verify.install") != null) {
-            DEFAULT_ACTION = Action.VERIFY_INSTALLED;
-        } else if (System.getProperty("jpackage.verify.uninstall") != null) {
-            DEFAULT_ACTION = Action.VERIFY_UNINSTALLED;
-        } else {
-            DEFAULT_ACTION = Action.CREATE;
-        }
+        final String propertyName = "action";
+        String action = Optional.ofNullable(Test.getConfigProperty(propertyName)).orElse(
+                Action.CREATE.toString()).toLowerCase();
+        DEFAULT_ACTION = Stream.of(Action.values()).filter(
+                a -> a.toString().equals(action)).findFirst().orElseThrow(
+                        () -> new IllegalArgumentException(String.format(
+                                "Unrecognized value of %s property: [%s]",
+                                Test.getConfigPropertyName(propertyName), action)));
     }
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageType.java	Tue Sep 24 13:41:16 2019 -0400
@@ -24,8 +24,6 @@
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Supplier;
@@ -61,7 +59,7 @@
             Test.trace(String.format("Bundler %s supported", getName()));
         }
     }
-    
+
     PackageType(String bundleSuffix, String bundlerClass) {
         this(bundleSuffix.substring(1), bundleSuffix, bundlerClass);
     }
@@ -96,15 +94,13 @@
     private static boolean isBundlerSupported(String bundlerClass) {
         try {
             Class clazz = Class.forName(bundlerClass);
-            Method isSupported = clazz.getDeclaredMethod("isSupported");
-            return ((Boolean) isSupported.invoke(clazz));
+            Method supported = clazz.getMethod("supported", boolean.class);
+            return ((Boolean) supported.invoke(clazz.newInstance(), true));
         } catch (ClassNotFoundException ex) {
             return false;
-        } catch (IllegalAccessException | InvocationTargetException ex) {
+        } catch (InstantiationException | NoSuchMethodException
+                | IllegalAccessException | InvocationTargetException ex) {
             throw new RuntimeException(ex);
-        } catch (NoSuchMethodException ex) {
-            // Not all bundler classes has isSupported() method.
-            return true;
         }
     }
 
@@ -118,28 +114,28 @@
     public final static Set<PackageType> NATIVE = Stream.concat(
             Stream.concat(LINUX.stream(), WINDOWS.stream()),
             MAC.stream()).collect(Collectors.toUnmodifiableSet());
-    
+
     public final static PackageType DEFAULT = ((Supplier<PackageType>) () -> {
         if (Test.isLinux()) {
             return LINUX.stream().filter(v -> v.isSupported()).findFirst().orElseThrow();
         }
-        
+
         if (Test.isWindows()) {
             return WIN_EXE;
         }
-        
+
         if (Test.isOSX()) {
             return MAC_DMG;
         }
-        
+
         throw new IllegalStateException("Unknwon platform");
     }).get();
-    
+
     private final static class Inner {
-        
+
         private final static Set<String> DISABLED_PACKAGERS = Stream.of(
                 Optional.ofNullable(
-                        System.getProperty("jpackage.test.disabledPackagers")).orElse(
+                        Test.getConfigProperty("disabledPackagers")).orElse(
                         "").split(",")).collect(Collectors.toUnmodifiableSet());
     }
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Test.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Test.java	Tue Sep 24 13:41:16 2019 -0400
@@ -22,8 +22,11 @@
  */
 package jdk.jpackage.test;
 
-import java.io.File;
+import java.io.BufferedOutputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -33,11 +36,17 @@
 import java.nio.file.WatchEvent;
 import java.nio.file.WatchKey;
 import java.nio.file.WatchService;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import jdk.jpackage.internal.IOUtils;
 
-public class Test {
+final public class Test {
 
     public static final Path TEST_SRC_ROOT = new Supplier<Path>() {
         @Override
@@ -55,6 +64,62 @@
         }
     }.get();
 
+    private static class Instance implements AutoCloseable {
+        Instance(String args[]) {
+            assertCount = 0;
+
+            name = enclosingMainMethodClass().getSimpleName();
+            extraLogStream = openLogStream();
+
+            currentTest = this;
+
+            log(String.format("[ RUN      ] %s", name));
+        }
+
+        @Override
+        public void close() {
+            log(String.format("%s %s; checks=%d",
+                    success ? "[       OK ]" : "[  FAILED  ]", name, assertCount));
+
+            if (extraLogStream != null) {
+                extraLogStream.close();
+            }
+        }
+
+        void notifyAssert() {
+            assertCount++;
+        }
+
+        void notifySuccess() {
+            success = true;
+        }
+
+        private int assertCount;
+        private boolean success;
+        private final String name;
+        private final PrintStream extraLogStream;
+    }
+
+    public static void run(String args[], TestBody action) {
+        if (currentTest != null) {
+            throw new IllegalStateException(
+                    "Unexpeced nested or concurrent Test.run() call");
+        }
+
+        try (Instance instance = new Instance(args)) {
+            action.run();
+            instance.notifySuccess();
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        } finally {
+            currentTest = null;
+        }
+    }
+
+    public static interface TestBody {
+        public void run() throws Exception;
+    }
+
     public static Path workDir() {
         return Path.of(".");
     }
@@ -67,6 +132,20 @@
         return workDir().resolve("output");
     }
 
+    static Class enclosingMainMethodClass() {
+        StackTraceElement st[] = Thread.currentThread().getStackTrace();
+        for (StackTraceElement ste : st) {
+            if ("main".equals(ste.getMethodName())) {
+                try {
+                    return Class.forName(ste.getClassName());
+                } catch (ClassNotFoundException ex) {
+                    throw new RuntimeException(ex);
+                }
+            }
+        }
+        return null;
+    }
+
     static boolean isWindows() {
         return (OS.contains("win"));
     }
@@ -80,7 +159,39 @@
     }
 
     static private void log(String v) {
-        System.err.println(v);
+        System.out.println(v);
+        if (currentTest != null && currentTest.extraLogStream != null) {
+            currentTest.extraLogStream.println(v);
+        }
+    }
+
+    public static Class getTestClass () {
+        return enclosingMainMethodClass();
+    }
+
+    public static void createPropertiesFile(Path propsFilename,
+            Collection<Map.Entry<String, String>> props) {
+        trace(String.format("Create [%s] properties file...",
+                propsFilename.toAbsolutePath().normalize()));
+        try {
+            Files.write(propsFilename, props.stream().peek(e -> trace(
+                    String.format("%s=%s", e.getKey(), e.getValue()))).map(
+                    e -> String.format("%s=%s", e.getKey(), e.getValue())).collect(
+                            Collectors.toList()));
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
+        trace("Done");
+    }
+
+    public static void createPropertiesFile(Path propsFilename,
+            Map.Entry<String, String>... props) {
+        createPropertiesFile(propsFilename, List.of(props));
+    }
+
+    public static void createPropertiesFile(Path propsFilename,
+            Map<String, String> props) {
+        createPropertiesFile(propsFilename, props.entrySet());
     }
 
     public static void trace(String v) {
@@ -100,12 +211,54 @@
         throw new AssertionError(v);
     }
 
+    private static final String TEMP_FILE_PREFIX = null;
+
     public static Path createTempDirectory() throws IOException {
-        return Files.createTempDirectory("jpackage_");
+        return Files.createTempDirectory(workDir(), TEMP_FILE_PREFIX);
     }
 
     public static Path createTempFile(String suffix) throws IOException {
-        return File.createTempFile("jpackage_", suffix).toPath();
+        return Files.createTempFile(workDir(), TEMP_FILE_PREFIX, suffix);
+    }
+
+    public static void withTempFile(String suffix, Consumer<Path> action) {
+        Path tempFile = null;
+        boolean keepIt = true;
+        try {
+            tempFile = createTempFile(suffix);
+            action.accept(tempFile);
+            keepIt = false;
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        } finally {
+            if (tempFile != null && !keepIt) {
+                try {
+                    Files.deleteIfExists(tempFile);
+                } catch (IOException ex) {
+                    throw new RuntimeException(ex);
+                }
+            }
+        }
+    }
+
+    public static void withTempDirectory(Consumer<Path> action) {
+        Path tempDir = null;
+        boolean keepIt = true;
+        try {
+            tempDir = createTempDirectory();
+            action.accept(tempDir);
+            keepIt = false;
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        } finally {
+            try {
+                if (tempDir != null && tempDir.toFile().isDirectory() && !keepIt) {
+                    IOUtils.deleteRecursive(tempDir.toFile());
+                }
+            } catch (IOException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
     }
 
     public static void waitForFileCreated(Path fileToWaitFor,
@@ -166,7 +319,8 @@
         return msg;
     }
 
-    public static void assertEquals(int expected, int actual, String msg) {
+    public static void assertEquals(long expected, long actual, String msg) {
+        currentTest.notifyAssert();
         if (expected != actual) {
             error(concatMessages(String.format(
                     "Expected [%d]. Actual [%d]", expected, actual),
@@ -176,21 +330,8 @@
         traceAssert(String.format("assertEquals(%d): %s", expected, msg));
     }
 
-    public static void assertEquals(String expected, String actual, String msg) {
-        if (expected == null && actual == null) {
-            return;
-        }
-
-        if (actual == null || !expected.equals(actual)) {
-            error(concatMessages(String.format(
-                    "Expected [%s]. Actual [%s]", expected, actual),
-                    msg));
-        }
-
-        traceAssert(String.format("assertEquals(%s): %s", expected, msg));
-    }
-
-    public static void assertNotEquals(int expected, int actual, String msg) {
+    public static void assertNotEquals(long expected, long actual, String msg) {
+        currentTest.notifyAssert();
         if (expected == actual) {
             error(concatMessages(String.format("Unexpected [%d] value", actual),
                     msg));
@@ -200,7 +341,33 @@
                 actual, msg));
     }
 
+    public static void assertEquals(String expected, String actual, String msg) {
+        currentTest.notifyAssert();
+        if ((actual != null && !actual.equals(expected))
+                || (expected != null && !expected.equals(actual))) {
+            error(concatMessages(String.format(
+                    "Expected [%s]. Actual [%s]", expected, actual),
+                    msg));
+        }
+
+        traceAssert(String.format("assertEquals(%s): %s", expected, msg));
+    }
+
+    public static void assertNotEquals(String expected, String actual, String msg) {
+        currentTest.notifyAssert();
+        if ((actual != null && !actual.equals(expected))
+                || (expected != null && !expected.equals(actual))) {
+
+            traceAssert(String.format("assertNotEquals(%s, %s): %s", expected,
+                actual, msg));
+            return;
+        }
+
+        error(concatMessages(String.format("Unexpected [%s] value", actual), msg));
+    }
+
     public static void assertNull(Object value, String msg) {
+        currentTest.notifyAssert();
         if (value != null) {
             error(concatMessages(String.format("Unexpected not null value [%s]",
                     value), msg));
@@ -210,6 +377,7 @@
     }
 
     public static void assertNotNull(Object value, String msg) {
+        currentTest.notifyAssert();
         if (value == null) {
             error(concatMessages("Unexpected null value", msg));
         }
@@ -218,6 +386,7 @@
     }
 
     public static void assertTrue(boolean actual, String msg) {
+        currentTest.notifyAssert();
         if (!actual) {
             error(concatMessages("Unexpected FALSE", msg));
         }
@@ -226,6 +395,7 @@
     }
 
     public static void assertFalse(boolean actual, String msg) {
+        currentTest.notifyAssert();
         if (actual) {
             error(concatMessages("Unexpected TRUE", msg));
         }
@@ -268,24 +438,67 @@
     }
 
     public static void assertUnexpected(String msg) {
+        currentTest.notifyAssert();
         error(concatMessages("Unexpected", msg));
     }
 
+    private static PrintStream openLogStream() {
+        if (LOG_FILE == null) {
+            return null;
+        }
+
+        try {
+            return new PrintStream(new FileOutputStream(LOG_FILE.toFile(), true));
+        } catch (FileNotFoundException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private static Instance currentTest;
+
     private static final boolean TRACE;
     private static final boolean TRACE_ASSERTS;
 
+    static final boolean VERBOSE_JPACKAGE;
+
+    static String getConfigProperty(String propertyName) {
+        return System.getProperty(getConfigPropertyName(propertyName));
+    }
+
+    static String getConfigPropertyName(String propertyName) {
+        return "jpackage.test." + propertyName;
+    }
+
+    static final Path LOG_FILE = new Supplier<Path>() {
+        @Override
+        public Path get() {
+            String val = getConfigProperty("logfile");
+            if (val == null) {
+                return null;
+            }
+            return Path.of(val);
+        }
+    }.get();
+
     static {
-        String val = System.getProperty("jpackage.test.suppress-logging");
+        String val = getConfigProperty("suppress-logging");
         if (val == null) {
             TRACE = true;
             TRACE_ASSERTS = true;
+            VERBOSE_JPACKAGE = true;
+        } else if ("all".equals(val.toLowerCase())) {
+            TRACE = false;
+            TRACE_ASSERTS = false;
+            VERBOSE_JPACKAGE = false;
         } else {
             Set<String> logOptions = Set.of(val.toLowerCase().split(","));
             TRACE = !(logOptions.contains("trace") || logOptions.contains("t"));
             TRACE_ASSERTS = !(logOptions.contains("assert") || logOptions.contains(
                     "a"));
+            VERBOSE_JPACKAGE = !(logOptions.contains("jpackage") || logOptions.contains(
+                    "jp"));
         }
     }
 
     private static final String OS = System.getProperty("os.name").toLowerCase();
-}
+}
\ No newline at end of file
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java	Tue Sep 24 13:41:16 2019 -0400
@@ -97,7 +97,7 @@
 
         private void verifyStartMenuShortcut() {
             boolean appInstalled = cmd.launcherInstallationPath().toFile().exists();
-            if (cmd.hasArgument("--win-menu") || !cmd.hasArgument("--win-shortcut")) {
+            if (cmd.hasArgument("--win-menu")) {
                 if (isUserLocalInstall(cmd)) {
                     verifyUserLocalStartMenuShortcut(appInstalled);
                     verifySystemStartMenuShortcut(false);
@@ -129,12 +129,11 @@
         }
 
         private void verifyFileAssociationsRegistry() {
-            Path faFile = cmd.getArgumentValue("--file-associations",
-                    () -> (Path) null, Path::of);
-            if (faFile == null) {
-                return;
-            }
+            Stream.of(cmd.getAllArgumentValues("--file-associations")).map(
+                    Path::of).forEach(this::verifyFileAssociationsRegistry);
+        }
 
+        private void verifyFileAssociationsRegistry(Path faFile) {
             boolean appInstalled = cmd.launcherInstallationPath().toFile().exists();
             try {
                 Test.trace(String.format(
--- a/test/jdk/tools/jpackage/linux/AppCategoryTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/linux/AppCategoryTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 
@@ -49,19 +50,21 @@
  */
 public class AppCategoryTest {
 
-    public static void main(String[] args) throws Exception {
+    public static void main(String[] args) {
         final String CATEGORY = "Foo";
 
-        new PackageTest()
-        .forTypes(PackageType.LINUX)
-        .configureHelloApp()
-        .addInitializer(cmd -> {
-            cmd.addArguments("--linux-app-category", CATEGORY);
-        })
-        .forTypes(PackageType.LINUX_DEB)
-        .addBundlePropertyVerifier("Section", CATEGORY)
-        .forTypes(PackageType.LINUX_RPM)
-        .addBundlePropertyVerifier("Group", CATEGORY)
-        .run();
+        Test.run(args, () -> {
+            new PackageTest()
+            .forTypes(PackageType.LINUX)
+            .configureHelloApp()
+            .addInitializer(cmd -> {
+                cmd.addArguments("--linux-app-category", CATEGORY);
+            })
+            .forTypes(PackageType.LINUX_DEB)
+            .addBundlePropertyVerifier("Section", CATEGORY)
+            .forTypes(PackageType.LINUX_RPM)
+            .addBundlePropertyVerifier("Group", CATEGORY)
+            .run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/linux/BundleNameTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/linux/BundleNameTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 
@@ -49,19 +50,21 @@
  */
 public class BundleNameTest {
 
-    public static void main(String[] args) throws Exception {
+    public static void main(String[] args) {
         final String PACKAGE_NAME = "quickbrownfox2";
 
-        new PackageTest()
-        .forTypes(PackageType.LINUX)
-        .configureHelloApp()
-        .addInitializer(cmd -> {
-            cmd.addArguments("--linux-package-name", PACKAGE_NAME);
-        })
-        .forTypes(PackageType.LINUX_DEB)
-        .addBundlePropertyVerifier("Package", PACKAGE_NAME)
-        .forTypes(PackageType.LINUX_RPM)
-        .addBundlePropertyVerifier("Name", PACKAGE_NAME)
-        .run();
+        Test.run(args, () -> {
+            new PackageTest()
+            .forTypes(PackageType.LINUX)
+            .configureHelloApp()
+            .addInitializer(cmd -> {
+                cmd.addArguments("--linux-package-name", PACKAGE_NAME);
+            })
+            .forTypes(PackageType.LINUX_DEB)
+            .addBundlePropertyVerifier("Package", PACKAGE_NAME)
+            .forTypes(PackageType.LINUX_RPM)
+            .addBundlePropertyVerifier("Name", PACKAGE_NAME)
+            .run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/linux/LicenseTypeTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/linux/LicenseTypeTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 
@@ -44,14 +45,16 @@
  */
 public class LicenseTypeTest {
 
-    public static void main(String[] args) throws Exception {
+    public static void main(String[] args) {
         final String LICENSE_TYPE = "JP_LICENSE_TYPE";
 
-        new PackageTest().forTypes(PackageType.LINUX_RPM).configureHelloApp()
-        .addInitializer(cmd -> {
-            cmd.addArguments("--linux-rpm-license-type", LICENSE_TYPE);
-        })
-        .addBundlePropertyVerifier("License", LICENSE_TYPE)
-        .run();
+        Test.run(args, () -> {
+            new PackageTest().forTypes(PackageType.LINUX_RPM).configureHelloApp()
+            .addInitializer(cmd -> {
+                cmd.addArguments("--linux-rpm-license-type", LICENSE_TYPE);
+            })
+            .addBundlePropertyVerifier("License", LICENSE_TYPE)
+            .run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/linux/MaintainerTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/linux/MaintainerTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -46,19 +46,21 @@
  */
 public class MaintainerTest {
 
-    public static void main(String[] args) throws Exception {
+    public static void main(String[] args) {
         final String MAINTAINER = "jpackage-test@java.com";
 
-        new PackageTest().forTypes(PackageType.LINUX_DEB).configureHelloApp()
-        .addInitializer(cmd -> {
-            cmd.addArguments("--linux-deb-maintainer", MAINTAINER);
-        })
-        .addBundlePropertyVerifier("Maintainer", (propName, propValue) -> {
-            String lookupValue = "<" + MAINTAINER + ">";
-            Test.assertTrue(propValue.endsWith(lookupValue),
-                    String.format("Check value of %s property [%s] ends with %s",
-                            propName, propValue, lookupValue));
-        })
-        .run();
+        Test.run(args, () -> {
+            new PackageTest().forTypes(PackageType.LINUX_DEB).configureHelloApp()
+            .addInitializer(cmd -> {
+                cmd.addArguments("--linux-deb-maintainer", MAINTAINER);
+            })
+            .addBundlePropertyVerifier("Maintainer", (propName, propValue) -> {
+                String lookupValue = "<" + MAINTAINER + ">";
+                Test.assertTrue(propValue.endsWith(lookupValue),
+                        String.format("Check value of %s property [%s] ends with %s",
+                                propName, propValue, lookupValue));
+            })
+            .run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/linux/PackageDepsTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/linux/PackageDepsTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 
@@ -56,26 +57,28 @@
     // produced by jtreg tests install/uninstall packages in the right order.
     static class APackageDepsTestPrereq {
 
-        public static void main(String[] args) throws Exception {
+        public static void main(String[] args) {
             new PackageTest().forTypes(PackageType.LINUX).configureHelloApp().run();
         }
     }
 
-    public static void main(String[] args) throws Exception {
+    public static void main(String[] args) {
         final String PREREQ_PACKAGE_NAME = "apackagedepstestprereq";
 
-        APackageDepsTestPrereq.main(args);
+        Test.run(args, () -> {
+            APackageDepsTestPrereq.main(args);
 
-        new PackageTest()
-        .forTypes(PackageType.LINUX)
-        .configureHelloApp()
-        .addInitializer(cmd -> {
-            cmd.addArguments("--linux-package-deps", PREREQ_PACKAGE_NAME);
-        })
-        .forTypes(PackageType.LINUX_DEB)
-        .addBundlePropertyVerifier("Depends", PREREQ_PACKAGE_NAME)
-        .forTypes(PackageType.LINUX_RPM)
-        .addBundlePropertyVerifier("Requires", PREREQ_PACKAGE_NAME)
-        .run();
+            new PackageTest()
+            .forTypes(PackageType.LINUX)
+            .configureHelloApp()
+            .addInitializer(cmd -> {
+                cmd.addArguments("--linux-package-deps", PREREQ_PACKAGE_NAME);
+            })
+            .forTypes(PackageType.LINUX_DEB)
+            .addBundlePropertyVerifier("Depends", PREREQ_PACKAGE_NAME)
+            .forTypes(PackageType.LINUX_RPM)
+            .addBundlePropertyVerifier("Requires", PREREQ_PACKAGE_NAME)
+            .run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/linux/ReleaseTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/linux/ReleaseTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,8 +21,8 @@
  * questions.
  */
 
+import jdk.jpackage.test.PackageType;
 import jdk.jpackage.test.PackageTest;
-import jdk.jpackage.test.PackageType;
 import jdk.jpackage.test.Test;
 
 
@@ -49,23 +49,25 @@
  */
 public class ReleaseTest {
 
-    public static void main(String[] args) throws Exception {
+    public static void main(String[] args) {
         final String RELEASE = "Rc3";
 
-        new PackageTest()
-        .forTypes(PackageType.LINUX)
-        .configureHelloApp()
-        .addInitializer(cmd -> {
-            cmd.addArguments("--linux-app-release", RELEASE);
-        })
-        .forTypes(PackageType.LINUX_RPM)
-        .addBundlePropertyVerifier("Release", RELEASE)
-        .forTypes(PackageType.LINUX_DEB)
-        .addBundlePropertyVerifier("Version", (propName, propValue) -> {
-            Test.assertTrue(propValue.endsWith("-" + RELEASE),
-                    String.format("Check value of %s property [%s] ends with %s",
-                            propName, propValue, RELEASE));
-        })
-        .run();
+        Test.run(args, () -> {
+            new PackageTest()
+            .forTypes(PackageType.LINUX)
+            .configureHelloApp()
+            .addInitializer(cmd -> {
+                cmd.addArguments("--linux-app-release", RELEASE);
+            })
+            .forTypes(PackageType.LINUX_RPM)
+            .addBundlePropertyVerifier("Release", RELEASE)
+            .forTypes(PackageType.LINUX_DEB)
+            .addBundlePropertyVerifier("Version", (propName, propValue) -> {
+                Test.assertTrue(propValue.endsWith("-" + RELEASE),
+                        String.format("Check value of %s property [%s] ends with %s",
+                                propName, propValue, RELEASE));
+            })
+            .run();
+        });
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/linux/ShortcutHintTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.util.Map;
+import java.nio.file.Path;
+import jdk.jpackage.test.FileAssociations;
+import jdk.jpackage.test.PackageType;
+import jdk.jpackage.test.PackageTest;
+import jdk.jpackage.test.Test;
+
+/**
+ * Test --linux-shortcut parameter. Output of the test should be
+ * shortcuthinttest_1.0-1_amd64.deb or shortcuthinttest-1.0-1.amd64.rpm package
+ * bundle. The output package should provide the same functionality as the
+ * default package and also create a desktop shortcut.
+ *
+ * Finding a shortcut of the application launcher through GUI depends on desktop
+ * environment.
+ *
+ * deb:
+ * Search online for `Ways To Open A Ubuntu Application` for instructions.
+ *
+ * rpm:
+ *
+ */
+
+/*
+ * @test
+ * @summary jpackage with --linux-shortcut
+ * @library ../helpers
+ * @requires (os.family == "linux")
+ * @modules jdk.jpackage/jdk.jpackage.internal
+ * @run main/othervm/timeout=360 -Xmx512m ShortcutHintTest
+ * @run main/othervm/timeout=360 -Xmx512m ShortcutHintTest testCustomIcon
+ * @run main/othervm/timeout=360 -Xmx512m ShortcutHintTest testFileAssociations
+ * @run main/othervm/timeout=360 -Xmx512m ShortcutHintTest testAdditionaltLaunchers
+ */
+public class ShortcutHintTest {
+
+    public static void main(String[] args) {
+        Test.run(args, () -> {
+            if (args.length != 0) {
+                Test.getTestClass().getDeclaredMethod(args[0]).invoke(null);
+                return;
+            }
+
+            createTest(null).addInitializer(cmd -> {
+                cmd.addArgument("--linux-shortcut");
+            }).run();
+        });
+    }
+
+    private static PackageTest createTest(String name) {
+        PackageTest reply = new PackageTest()
+                .forTypes(PackageType.LINUX)
+                .configureHelloApp()
+                .addBundleDesktopIntegrationVerifier(true);
+        if (name != null) {
+            reply.addInitializer(cmd -> cmd.setArgumentValue("--name",
+                    String.format("%s8%s", Test.getTestClass().getSimpleName(),
+                            name)));
+        }
+        return reply;
+    }
+
+    /**
+     * Adding `--icon` to jpackage command line should create desktop shortcut
+     * even though `--linux-shortcut` is omitted.
+     */
+    static void testCustomIcon() {
+        createTest(new Object() {
+        }.getClass().getEnclosingMethod().getName()).addInitializer(cmd -> {
+            cmd.setFakeRuntime();
+            cmd.addArguments("--icon", Test.TEST_SRC_ROOT.resolve(
+                    "apps/dukeplug.png"));
+        }).run();
+    }
+
+    /**
+     * Adding `--file-associations` to jpackage command line should create
+     * desktop shortcut even though `--linux-shortcut` is omitted.
+     */
+    static void testFileAssociations() {
+        createTest(new Object() {
+        }.getClass().getEnclosingMethod().getName()).addInitializer(cmd -> {
+            cmd.setFakeRuntime();
+
+            FileAssociations fa = new FileAssociations(
+                    "ShortcutHintTest_testFileAssociations");
+            fa.createFile();
+            cmd.addArguments("--file-associations", fa.getPropertiesFile());
+        }).run();
+    }
+
+    /**
+     * Additional launcher with icon should create desktop shortcut even though
+     * `--linux-shortcut` is omitted.
+     */
+    static void testAdditionaltLaunchers() {
+        createTest(new Object() {
+        }.getClass().getEnclosingMethod().getName()).addInitializer(cmd -> {
+            cmd.setFakeRuntime();
+
+            final String launcherName = "Foo";
+            final Path propsFile = Test.workDir().resolve(
+                    launcherName + ".properties");
+
+            cmd.addArguments("--add-launcher", String.format("%s=%s",
+                    launcherName, propsFile));
+
+            Test.createPropertiesFile(propsFile, Map.entry("icon",
+                    Test.TEST_SRC_ROOT.resolve("apps/dukeplug.png").toString()));
+        }).run();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/manage_packages.sh	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,185 @@
+#!/bin/bash
+
+#
+# Script to install/uninstall packages produced by jpackage jtreg
+# tests doing platform specific packaging.
+#
+# The script will install/uninstall all packages from the files
+# found in the current directory or the one specified with command line option.
+#
+# When jtreg jpackage tests are executed with jpackage.test.output
+# Java property set, produced package files (msi, exe, deb, rpm, etc.) will
+# be saved in the directory specified with this property.
+#
+# Usage example:
+# # Set directory where to save package files from jtreg jpackage tests
+# JTREG_OUTPUT_DIR=/tmp/jpackage_jtreg_packages
+#
+# # Run tests and fill $JTREG_OUTPUT_DIR directory with package files
+# jtreg -Djpackage.test.output=$JTREG_OUTPUT_DIR ...
+#
+# # Install all packages
+# manage_pachages.sh -d $JTREG_OUTPUT_DIR
+#
+# # Uninstall all packages
+# manage_pachages.sh -d $JTREG_OUTPUT_DIR -u
+#
+
+#
+# When using with MSI installers, Cygwin shell from which this script is
+# executed should be started as administrator. Otherwise silent installation
+# won't work.
+#
+
+# Fail fast
+set -e; set -o pipefail;
+
+
+help_usage ()
+{
+    echo "Usage: `basename $0` [OPTION]"
+    echo "Options:"
+    echo "  -h        - print this message"
+    echo "  -v        - verbose output"
+    echo "  -d <dir>  - path to directory where to look for package files"
+    echo "  -u        - uninstall packages instead of the default install"
+    echo "  -t        - dry run, print commands but don't execute them"
+}
+
+error ()
+{
+  echo "$@" > /dev/stderr
+}
+
+fatal ()
+{
+  error "$@"
+  exit 1
+}
+
+fatal_with_help_usage ()
+{
+  error "$@"
+  help_usage
+  exit 1
+}
+
+
+# Directory where to look for package files.
+package_dir=$PWD
+
+# Script debug.
+verbose=
+
+# Operation mode.
+mode=install
+
+dryrun=
+
+while getopts "vhd:ut" argname; do
+    case "$argname" in
+        v) verbose=yes;;
+        t) dryrun=yes;;
+        u) mode=uninstall;;
+        d) package_dir="$OPTARG";;
+        h) help_usage; exit 0;;
+        ?) help_usage; exit 1;;
+    esac
+done
+shift $(( OPTIND - 1 ))
+
+[ -d "$package_dir" ] || fatal_with_help_usage "Package directory [$package_dir] is not a directory"
+
+[ -z "$verbose" ] || set -x
+
+
+function find_packages_of_type ()
+{
+    # sort output alphabetically
+    find "$package_dir" -maxdepth 1 -type f -name '*.'"$1" | sort
+}
+
+function find_packages ()
+{
+    local package_suffixes=(deb rpm msi exe)
+    for suffix in "${package_suffixes[@]}"; do
+        if [ "$mode" == "uninstall" ]; then
+            packages=$(find_packages_of_type $suffix | tac)
+        else
+            packages=$(find_packages_of_type $suffix)
+        fi
+        if [ -n "$packages" ]; then
+            package_type=$suffix
+            break;
+        fi
+    done
+}
+
+
+# RPM
+install_cmd_rpm ()
+{
+    echo sudo rpm --install "$@"
+}
+uninstall_cmd_rpm ()
+{
+    local package_name=$(rpm -qp --queryformat '%{Name}' "$@")
+    echo sudo rpm -e "$package_name"
+}
+
+# DEB
+install_cmd_deb ()
+{
+    echo sudo dpkg -i "$@"
+}
+uninstall_cmd_deb ()
+{
+    local package_name=$(dpkg-deb -f "$@" Package)
+    echo sudo dpkg -r "$package_name"
+}
+
+# MSI
+install_cmd_msi ()
+{
+    echo msiexec /qn /norestart /i $(cygpath -w "$@")
+}
+uninstall_cmd_msi ()
+{
+    echo msiexec /qn /norestart /x $(cygpath -w "$@")
+}
+
+# EXE
+install_cmd_exe ()
+{
+    echo "$@"
+}
+uninstall_cmd_exe ()
+{
+    error No implemented
+}
+
+
+# Find packages
+packages=
+find_packages
+if [ -z "$packages" ]; then
+    echo "No packages found in $package_dir directory"
+    exit
+fi
+
+# Build list of commands to execute
+declare -a commands
+for p in $packages; do
+    commands[${#commands[@]}]=$(${mode}_cmd_${package_type} "$p")
+done
+
+if [ -z "$dryrun" ]; then
+    # Run commands
+    for cmd in "${commands[@]}"; do
+        echo Running: $cmd
+        $cmd || true;
+    done
+else
+    # Print commands
+    for cmd in "${commands[@]}"; do echo $cmd; done
+fi
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/run_tests.sh	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,278 @@
+#!/bin/bash
+
+#
+# Script to run jpackage tests.
+#
+
+
+# Fail fast
+set -e; set -o pipefail;
+
+
+# Link obtained from https://openjdk.java.net/jtreg/ page
+jtreg_bundle=https://ci.adoptopenjdk.net/view/Dependencies/job/jtreg/lastSuccessfulBuild/artifact/jtreg-4.2.0-tip.tar.gz
+workdir=/tmp/jpackage_jtreg_testing
+jtreg_jar=$workdir/jtreg/lib/jtreg.jar
+
+# Names of shared packaging tests to run
+share_package_test_names="
+  FileAssociationsTest
+  InstallDirTest
+  LicenseTest
+  SimplePackageTest
+  RuntimePackageTest
+  AdditionalLaunchersTest
+  AppImagePackageTest
+"
+mapfile -t packaging_tests_share < <(for t in $share_package_test_names; do echo test/jdk/tools/jpackage/share/$t.java; done)
+packaging_tests_windows=test/jdk/tools/jpackage/windows
+packaging_tests_linux=test/jdk/tools/jpackage/linux
+packaging_tests_mac=test/jdk/tools/jpackage/macosx
+
+case "$(uname -s)" in
+  Darwin)
+    tests=( "$packaging_tests_mac" );;
+  Linux)
+    tests=( "$packaging_tests_linux" );;
+  CYGWIN*|MINGW32*|MSYS*)
+    tests=( "$packaging_tests_windows" );;
+  *)
+    fatal Failed to detect OS type;;
+esac
+tests+=(${packaging_tests_share[@]})
+
+
+help_usage ()
+{
+  echo "Usage: `basename $0` [options] [test_names]"
+  echo "Options:"
+  echo "  -h              - print this message"
+  echo "  -v              - verbose output"
+  echo "  -c              - keep jtreg cache"
+  echo "  -d              - dry run. Print jtreg command line, but don't execute it"
+  echo "  -t <jdk>        - path to JDK to be tested [ mandatory ]"
+  echo "  -j <openjdk>    - path to local copy of openjdk repo with jpackage jtreg tests"
+  echo "                    Optional, default is openjdk repo where this script resides"
+  echo "  -o <outputdir>  - path to folder where to copy artifacts for testing."
+  echo "                    Optional, default is the current directory."
+  echo '  -r <runtimedir> - value for `jpackage.test.runtime-image` property.'
+  echo "                    Optional, for jtreg tests debug purposes only."
+  echo '  -l <logfile>    - value for `jpackage.test.logfile` property.'
+  echo "                    Optional, for jtreg tests debug purposes only."
+  echo "  -m <mode>       - mode to run jtreg tests."
+  echo '                    Should be one of `create`, `update`, `verify-install` or `verify-uninstall`.'
+  echo '                    Optional, default mode is `update`.'
+  echo '                    - `create`'
+  echo '                      Remove all package bundles from the output directory before running jtreg tests.'
+  echo '                    - `update`'
+  echo '                      Run jtreg tests and overrite existing package bundles in the output directory.'
+  echo '                    - `verify-install`'
+  echo '                      Verify installed packages created with the previous run of the script.'
+  echo '                    - `verify-uninstall`'
+  echo '                      Verify packages created with the previous run of the script were uninstalled cleanly.'
+  echo '                    - `print-default-tests`'
+  echo '                      Print default tests list and exit.'
+}
+
+error ()
+{
+  echo "$@" > /dev/stderr
+}
+
+fatal ()
+{
+  error "$@"
+  exit 1
+}
+
+fatal_with_help_usage ()
+{
+  error "$@"
+  help_usage
+  exit 1
+}
+
+if command -v cygpath &> /dev/null; then
+to_native_path ()
+{
+  cygpath -m "$@"
+}
+else
+to_native_path ()
+{
+  echo "$@"
+}
+fi
+
+exec_command ()
+{
+  if [ -n "$dry_run" ]; then
+    echo "$@"
+  else
+    eval "$@"
+  fi
+}
+
+expand_test_selector ()
+{
+  if [ -d "$open_jdk_with_jpackage_jtreg_tests/$1" ]; then
+    for java in $(find "$open_jdk_with_jpackage_jtreg_tests/$1" -maxdepth 1 -name '*.java'); do
+      ! grep -q '@test' "$java" || echo "$1/$(basename "$java")"
+    done
+  else
+    echo "$1"
+  fi
+}
+
+
+# Path to JDK to be tested.
+test_jdk=
+
+# Path to local copy of open jdk repo with jpackage jtreg tests
+# hg clone http://hg.openjdk.java.net/jdk/sandbox
+# cd sandbox; hg update -r JDK-8200758-branch
+open_jdk_with_jpackage_jtreg_tests=$(dirname $0)/../../../../
+
+# Directory where to save artifacts for testing.
+output_dir=$PWD
+
+# Script and jtreg debug.
+verbose=
+jtreg_verbose="-verbose:fail,error,summary"
+
+keep_jtreg_cache=
+
+# Mode in which to run jtreg tests
+mode=update
+
+# JVM extra arguments
+declare -a vm_args
+
+while getopts "vhdct:j:o:r:m:l:" argname; do
+  case "$argname" in
+    v) verbose=yes;;
+    d) dry_run=yes;;
+    c) keep_jtreg_cache=yes;;
+    t) test_jdk="$OPTARG";;
+    j) open_jdk_with_jpackage_jtreg_tests="$OPTARG";;
+    o) output_dir="$OPTARG";;
+    r) runtime_dir="$OPTARG";;
+    l) logfile="$OPTARG";;
+    m) mode="$OPTARG";;
+    h) help_usage; exit 0;;
+    ?) help_usage; exit 1;;
+  esac
+done
+shift $(( OPTIND - 1 ))
+
+[ -z "$verbose" ] || { set -x; jtreg_verbose=-va; }
+
+if [ -z "$open_jdk_with_jpackage_jtreg_tests" ]; then
+  fatal_with_help_usage "Path to openjdk repo with jpackage jtreg tests not specified"
+fi
+
+if [ "$mode" = "print-default-tests" ]; then
+  exec_command for t in ${tests[@]}";" do expand_test_selector '$t;' done
+  exit
+fi
+
+if [ -z "$test_jdk" ]; then
+  fatal_with_help_usage Path to test JDK not specified
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  echo JAVA_HOME environment variable not set, will use java from test JDK [$test_jdk] to run jtreg
+  JAVA_HOME="$test_jdk"
+fi
+if [ ! -e "$JAVA_HOME/bin/java" ]; then
+  fatal JAVA_HOME variable is set to [$JAVA_HOME] value, but $JAVA_HOME/bin/java not found.
+fi
+
+if [ -n "$runtime_dir" ]; then
+  if [ ! -d "$runtime_dir" ]; then
+    fatal 'Value of `-r` option is set to non-existing directory'.
+  fi
+  vm_args+=("-Djpackage.test.runtime-image=$(to_native_path "$(cd "$runtime_dir" && pwd)")")
+fi
+
+if [ -n "$logfile" ]; then
+  if [ ! -d "$(dirname "$logfile")" ]; then
+    fatal 'Value of `-l` option specified a file in non-existing directory'.
+  fi
+  logfile="$(cd "$(dirname "$logfile")" && pwd)/$(basename "$logfile")"
+  vm_args+=("-Djpackage.test.logfile=$(to_native_path "$logfile")")
+fi
+
+if [ "$mode" = create ]; then
+  true
+elif [ "$mode" = update ]; then
+  true
+elif [ "$mode" = verify-install ]; then
+  vm_args+=("-Djpackage.test.action=$mode")
+elif [ "$mode" = verify-uninstall ]; then
+  vm_args+=("-Djpackage.test.action=$mode")
+else
+  fatal_with_help_usage 'Invalid value of -m option:' [$mode]
+fi
+
+
+# All remaining command line arguments are tests to run that should override the defaults
+[ $# -eq 0 ] || tests=($@)
+
+
+installJtreg ()
+{
+  # Install jtreg if missing
+  if [ ! -f "$jtreg_jar" ]; then
+    exec_command mkdir -p "$workdir"
+    exec_command "(" cd "$workdir" "&&" wget "$jtreg_bundle" "&&" tar -xzf "$(basename $jtreg_bundle)" ";" rm -f "$(basename $jtreg_bundle)" ")"
+  fi
+}
+
+
+preRun ()
+{
+  local xargs_args=(-t --no-run-if-empty rm)
+  if [ -n "$dry_run" ]; then
+    xargs_args=(--no-run-if-empty echo rm)
+  fi
+
+  if [ ! -d "$output_dir" ]; then
+    exec_command mkdir -p "$output_dir"
+  fi
+  [ ! -d "$output_dir" ] || output_dir=$(cd "$output_dir" && pwd)
+
+  # Clean output directory
+  [ "$mode" != "create" ] || find $output_dir -maxdepth 1 -type f -name '*.exe' -or -name '*.msi' -or -name '*.rpm' -or -name '*.deb' | xargs "${xargs_args[@]}"
+}
+
+
+run ()
+{
+  local jtreg_cmdline=(\
+    $JAVA_HOME/bin/java -jar $(to_native_path "$jtreg_jar") \
+    "-Djpackage.test.output=$(to_native_path "$output_dir")" \
+    "${vm_args[@]}" \
+    -nr \
+    "$jtreg_verbose" \
+    -retain:all \
+    -automatic \
+    -ignore:run \
+    -testjdk:"$(to_native_path $test_jdk)" \
+    -dir:"$(to_native_path $open_jdk_with_jpackage_jtreg_tests)" \
+    -reportDir:"$(to_native_path $workdir/run/results)" \
+    -workDir:"$(to_native_path $workdir/run/support)" \
+    "${tests[@]}" \
+  )
+
+  # Clear previous results
+  [ -n "$keep_jtreg_cache" ] || exec_command rm -rf "$workdir"/run
+
+  # Run jpackage jtreg tests to create artifacts for testing
+  exec_command ${jtreg_cmdline[@]}
+}
+
+
+installJtreg
+preRun
+run
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/share/AdditionalLaunchersTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.List;
+import java.util.Optional;
+import java.lang.invoke.MethodHandles;
+import jdk.jpackage.test.HelloApp;
+import jdk.jpackage.test.PackageTest;
+import jdk.jpackage.test.PackageType;
+import jdk.jpackage.test.FileAssociations;
+import jdk.jpackage.test.Test;
+
+/**
+ * Test --add-launcher parameter. Output of the test should be
+ * additionallauncherstest*.* installer. The output installer should provide the
+ * same functionality as the default installer (see description of the default
+ * installer in SimplePackageTest.java) plus install three extra application
+ * launchers.
+ */
+
+/*
+ * @test
+ * @summary jpackage with --add-launcher
+ * @library ../helpers
+ * @modules jdk.jpackage/jdk.jpackage.internal
+ * @run main/othervm/timeout=360 -Xmx512m AdditionalLaunchersTest
+ */
+public class AdditionalLaunchersTest {
+
+    public static void main(String[] args) {
+        Test.run(args, () -> {
+            FileAssociations fa = new FileAssociations(
+                    MethodHandles.lookup().lookupClass().getSimpleName());
+
+            // Configure a bunch of additional launchers and also setup
+            // file association to make sure it will be linked only to the main
+            // launcher.
+
+            PackageTest packageTest = new PackageTest().configureHelloApp()
+            .addInitializer(cmd -> {
+                fa.createFile();
+                cmd.addArguments("--file-associations", fa.getPropertiesFile());
+                cmd.addArguments("--arguments", "Duke", "--arguments", "is",
+                        "--arguments", "the", "--arguments", "King");
+            });
+
+            packageTest.addHelloAppFileAssociationsVerifier(fa);
+
+            new AdditionalLauncher("Baz2").setArguments().applyTo(packageTest);
+            new AdditionalLauncher("foo").setArguments("yep!").applyTo(packageTest);
+
+            AdditionalLauncher barLauncher = new AdditionalLauncher("Bar").setArguments(
+                    "one", "two", "three");
+            packageTest.forTypes(PackageType.LINUX).addInitializer(cmd -> {
+                barLauncher.setIcon(Test.TEST_SRC_ROOT.resolve("apps/dukeplug.png"));
+            });
+            barLauncher.applyTo(packageTest);
+
+            packageTest.run();
+        });
+    }
+
+    private static Path replaceFileName(Path path, String newFileName) {
+        String fname = path.getFileName().toString();
+        int lastDotIndex = fname.lastIndexOf(".");
+        if (lastDotIndex != -1) {
+            fname = newFileName + fname.substring(lastDotIndex);
+        }
+        return path.getParent().resolve(fname);
+    }
+
+    static class AdditionalLauncher {
+
+        AdditionalLauncher(String name) {
+            this.name = name;
+        }
+
+        AdditionalLauncher setArguments(String... args) {
+            arguments = List.of(args);
+            return this;
+        }
+
+        AdditionalLauncher setIcon(Path iconPath) {
+            icon = iconPath;
+            return this;
+        }
+
+        void applyTo(PackageTest test) {
+            final Path propsFile = Test.workDir().resolve(name + ".properties");
+
+            test.addInitializer(cmd -> {
+                cmd.addArguments("--add-launcher", String.format("%s=%s", name,
+                        propsFile));
+
+                Map<String, String> properties = new HashMap<>();
+                if (arguments != null) {
+                    properties.put("arguments", String.join(" ",
+                            arguments.toArray(String[]::new)));
+                }
+
+                if (icon != null) {
+                    properties.put("icon", icon.toAbsolutePath().toString());
+                }
+
+                Test.createPropertiesFile(propsFile, properties);
+            });
+            test.addInstallVerifier(cmd -> {
+                Path launcherPath = replaceFileName(
+                        cmd.launcherInstallationPath(), name);
+
+                Test.assertExecutableFileExists(launcherPath, true);
+
+                if (cmd.isFakeRuntimeInstalled(String.format(
+                        "Not running %s launcher", launcherPath))) {
+                    return;
+                }
+                HelloApp.executeAndVerifyOutput(launcherPath,
+                        Optional.ofNullable(arguments).orElse(List.of()).toArray(
+                                String[]::new));
+            });
+            test.addUninstallVerifier(cmd -> {
+                Path launcherPath = replaceFileName(
+                        cmd.launcherInstallationPath(), name);
+
+                Test.assertExecutableFileExists(launcherPath, false);
+            });
+        }
+
+        private List<String> arguments;
+        private Path icon;
+        private final String name;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/share/AppImagePackageTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.nio.file.Path;
+import jdk.jpackage.test.Test;
+import jdk.jpackage.test.PackageTest;
+import jdk.jpackage.test.PackageType;
+import jdk.jpackage.test.JPackageCommand;
+
+/**
+ * Test --app-image parameter. The output installer should provide the same
+ * functionality as the default installer (see description of the default
+ * installer in SimplePackageTest.java)
+ */
+
+/*
+ * @test
+ * @summary jpackage with --app-image
+ * @library ../helpers
+ * @modules jdk.jpackage/jdk.jpackage.internal
+ * @run main/othervm/timeout=360 -Xmx512m AppImagePackageTest
+ */
+public class AppImagePackageTest {
+
+    public static void main(String[] args) {
+        Test.run(args, () -> {
+            Path appimageOutput = Path.of("appimage");
+
+            JPackageCommand appImageCmd = JPackageCommand.helloAppImage()
+                    .setArgumentValue("--dest", appimageOutput)
+                    .addArguments("--package-type", "app-image");
+
+            PackageTest packageTest = new PackageTest();
+            if (packageTest.getAction() == PackageTest.Action.CREATE) {
+                appImageCmd.execute();
+            }
+
+            packageTest.addInitializer(cmd -> {
+                Path appimageInput = appimageOutput.resolve(appImageCmd.name());
+
+                if (PackageType.MAC.contains(cmd.packageType())) {
+                    // Why so complicated on macOS?
+                    appimageInput = Path.of(appimageInput.toString() + ".app");
+                    cmd.addArguments("--identifier", appImageCmd.name());
+                }
+
+                cmd.addArguments("--app-image", appimageInput);
+                cmd.removeArgument("--input");
+            }).addBundleDesktopIntegrationVerifier(false).run();
+        });
+    }
+}
--- a/test/jdk/tools/jpackage/share/FileAssociationsTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/share/FileAssociationsTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,32 +21,26 @@
  * questions.
  */
 
-import java.awt.Desktop;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import jdk.jpackage.test.HelloApp;
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
-import jdk.jpackage.test.Test;
+import jdk.jpackage.test.FileAssociations;
 
 /**
- * Test --file-associations parameter.
- * Output of the test should be fileassociationstest*.* installer.
- * The output installer should provide the same functionality as the default
- * installer (see description of the default installer in SimplePackageTest.java)
- * plus configure file associations.
- * After installation files with ".jptest1" suffix should be associated with
- * the test app.
+ * Test --file-associations parameter. Output of the test should be
+ * fileassociationstest*.* installer. The output installer should provide the
+ * same functionality as the default installer (see description of the default
+ * installer in SimplePackageTest.java) plus configure file associations. After
+ * installation files with ".jptest1" and ".jptest2" suffixes should be
+ * associated with the test app.
  *
  * Suggested test scenario is to create empty file with ".jptest1" suffix,
  * double click on it and make sure that test application was launched in
- * response to double click event with the path to test .jptest1 file
- * on the commend line.
+ * response to double click event with the path to test .jptest1 file on the
+ * commend line. The same applies to ".jptest2" suffix.
  *
- * On Linux use "echo > foo.jptest1" and not "touch foo.jptest1" to create
- * test file as empty files are always interpreted as plain text and will not
- * be opened with the test app. This is a known bug.
+ * On Linux use "echo > foo.jptest1" and not "touch foo.jptest1" to create test
+ * file as empty files are always interpreted as plain text and will not be
+ * opened with the test app. This is a known bug.
  */
 
 /*
@@ -57,67 +51,22 @@
  * @run main/othervm/timeout=360 -Xmx512m FileAssociationsTest
  */
 public class FileAssociationsTest {
-    public static void main(String[] args) throws Exception {
-        new PackageTest().configureHelloApp()
-        .addInitializer(cmd -> {
-            initFaPropsFile();
-            cmd.addArguments("--file-associations", FA_PROPS_FILE.toString());
-        })
-        .addInstallVerifier(cmd -> {
-            Path testFile = null;
-            try {
-                testFile = Test.createTempFile("." + FA_SUFFIX);
-                // Write something in test file.
-                // On Ubuntu and Oracle Linux empty files are considered
-                // plain text. Seems like a system bug.
-                //
-                // [asemenyu@spacewalk ~]$ rm gg.jptest1
-                // $ touch foo.jptest1
-                // $ xdg-mime query filetype foo.jptest1
-                // text/plain
-                // $ echo > foo.jptest1
-                // $ xdg-mime query filetype foo.jptest1
-                // application/x-jpackage-jptest1
-                //
-                Files.write(testFile, Arrays.asList(""));
+    public static void main(String[] args) {
+        Test.run(args, () -> {
+            PackageTest packageTest = new PackageTest();
 
-                final Path appOutput = Path.of(HelloApp.OUTPUT_FILENAME);
-                Files.deleteIfExists(appOutput);
-
-                Test.trace(String.format("Use desktop to open [%s] file", testFile));
-                Desktop.getDesktop().open(testFile.toFile());
-                Test.waitForFileCreated(appOutput, 7);
-
-                // Wait a little bit after file has been created to
-                // make sure there are no pending writes into it.
-                Thread.sleep(3000);
-                HelloApp.verifyOutputFile(appOutput, testFile.toString());
-            } catch (IOException | InterruptedException ex) {
-                throw new RuntimeException(ex);
-            } finally {
-                if (testFile != null) {
-                    try {
-                        Files.deleteIfExists(testFile);
-                    } catch (IOException ex) {
-                        throw new RuntimeException(ex);
-                    }
-                }
-            }
-        })
-        .run();
+            applyFileAssociations(packageTest, new FileAssociations("jptest1"));
+            applyFileAssociations(packageTest,
+                    new FileAssociations("jptest2").setFilename("fa2"));
+            packageTest.run();
+        });
     }
 
-    private static void initFaPropsFile() {
-        try {
-            Files.write(FA_PROPS_FILE, Arrays.asList(
-                "extension=" + FA_SUFFIX,
-                "mime-type=application/x-jpackage-" + FA_SUFFIX,
-                "description=jpackage test extention"));
-        } catch (IOException ex) {
-            throw new RuntimeException(ex);
-        }
+    private static void applyFileAssociations(PackageTest test,
+            FileAssociations fa) {
+        test.addInitializer(cmd -> {
+            fa.createFile();
+            cmd.addArguments("--file-associations", fa.getPropertiesFile());
+        }).addHelloAppFileAssociationsVerifier(fa);
     }
-
-    private final static String FA_SUFFIX = "jptest1";
-    private final static Path FA_PROPS_FILE = Test.workDir().resolve("fa.properties");
 }
--- a/test/jdk/tools/jpackage/share/InstallDirTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/share/InstallDirTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -25,6 +25,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.Supplier;
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 
@@ -57,32 +58,32 @@
  */
 public class InstallDirTest {
 
-    public static void main(String[] args) throws Exception {
-        final Map<PackageType, String> INSTALL_DIRS = new Supplier<Map<PackageType, String>>() {
+    public static void main(String[] args) {
+        final Map<PackageType, Path> INSTALL_DIRS = new Supplier<Map<PackageType, Path>>() {
             @Override
-            public Map<PackageType, String> get() {
-                Map<PackageType, String> reply = new HashMap<>();
-                reply.put(PackageType.WIN_MSI, Path.of("TestVendor",
-                        "InstallDirTest1234").toString());
+            public Map<PackageType, Path> get() {
+                Map<PackageType, Path> reply = new HashMap<>();
+                reply.put(PackageType.WIN_MSI, Path.of(
+                        "TestVendor\\InstallDirTest1234"));
                 reply.put(PackageType.WIN_EXE, reply.get(PackageType.WIN_MSI));
 
-                reply.put(PackageType.LINUX_DEB,
-                        Path.of("/opt", "jpackage").toString());
+                reply.put(PackageType.LINUX_DEB, Path.of("/opt/jpackage"));
                 reply.put(PackageType.LINUX_RPM,
                         reply.get(PackageType.LINUX_DEB));
 
-                reply.put(PackageType.MAC_PKG, Path.of("/Application",
-                        "jpackage").toString());
+                reply.put(PackageType.MAC_PKG, Path.of("/Application/jpackage"));
                 reply.put(PackageType.MAC_DMG, reply.get(PackageType.MAC_PKG));
 
                 return reply;
             }
         }.get();
 
-        new PackageTest().configureHelloApp()
-                .addInitializer(cmd -> {
-                    cmd.addArguments("--install-dir", INSTALL_DIRS.get(
-                            cmd.packageType()));
-                }).run();
+        Test.run(args, () -> {
+            new PackageTest().configureHelloApp()
+            .addInitializer(cmd -> {
+                cmd.addArguments("--install-dir", INSTALL_DIRS.get(
+                        cmd.packageType()));
+            }).run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/share/LicenseTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/share/LicenseTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -24,10 +24,13 @@
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.io.File;
+import java.util.List;
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 import jdk.jpackage.test.JPackageCommand;
+import jdk.jpackage.test.PackageType;
 import jdk.jpackage.test.PackageTest;
-import jdk.jpackage.test.PackageType;
 import jdk.jpackage.test.LinuxHelper;
 import jdk.jpackage.test.Executor;
 import jdk.jpackage.test.Test;
@@ -64,42 +67,42 @@
  * @run main/othervm/timeout=360 -Xmx512m LicenseTest
  */
 public class LicenseTest {
-    public static void main(String[] args) throws Exception {
-        new PackageTest().configureHelloApp()
-        .addInitializer(cmd -> {
-            cmd.addArguments("--license-file", LICENSE_FILE.toString());
-        })
-        .forTypes(PackageType.LINUX_DEB)
-        .addBundleVerifier(cmd -> {
-            verifyLicenseFileInLinuxPackage(cmd, debLicenseFile(cmd));
-        })
-        .addInstallVerifier(cmd -> {
-            verifyLicenseFileInstalledLinux(debLicenseFile(cmd));
-        })
-        .addUninstallVerifier(cmd -> {
-            verifyLicenseFileNotInstalledLinux(debLicenseFile(cmd));
-        })
-        .forTypes(PackageType.LINUX_RPM)
-        .addBundleVerifier(cmd -> {
-            verifyLicenseFileInLinuxPackage(cmd,rpmLicenseFile(cmd));
-        })
-        .addInstallVerifier(cmd -> {
-            verifyLicenseFileInstalledLinux(rpmLicenseFile(cmd));
-        })
-        .addUninstallVerifier(cmd -> {
-            verifyLicenseFileNotInstalledLinux(rpmLicenseFile(cmd));
-        })
-        .run();
-    }
+    public static void main(String[] args) {
+        Test.run(args, () -> {
+            new PackageTest().configureHelloApp()
+            .addInitializer(cmd -> {
+                cmd.addArguments("--license-file", LICENSE_FILE);
+            })
+            .forTypes(PackageType.LINUX_DEB)
+            .addBundleVerifier(cmd -> {
+                verifyLicenseFileInLinuxPackage(cmd, debLicenseFile(cmd));
+            })
+            .addInstallVerifier(cmd -> {
+                verifyLicenseFileInstalledDebian(debLicenseFile(cmd));
+            })
+            .addUninstallVerifier(cmd -> {
+                verifyLicenseFileNotInstalledLinux(debLicenseFile(cmd));
+            })
+            .forTypes(PackageType.LINUX_RPM)
+            .addBundleVerifier(cmd -> {
+                verifyLicenseFileInLinuxPackage(cmd,rpmLicenseFile(cmd));
+            })
+            .addInstallVerifier(cmd -> {
+                verifyLicenseFileInstalledRpm(rpmLicenseFile(cmd));
+            })
+            .addUninstallVerifier(cmd -> {
+                verifyLicenseFileNotInstalledLinux(rpmLicenseFile(cmd));
+            })
+            .run();
+        });
+     }
 
     private static Path rpmLicenseFile(JPackageCommand cmd) {
         final Path licenseRoot = Path.of(
                 new Executor()
                 .setExecutable("rpm")
                 .addArguments("--eval", "%{_defaultlicensedir}")
-                .saveFirstLineOfOutput()
-                .execute()
-                .assertExitCodeIsZero().getFirstLineOfOutput());
+                .executeAndGetFirstLineOfOutput());
         final Path licensePath = licenseRoot.resolve(String.format("%s-%s",
                 LinuxHelper.getPackageName(cmd), cmd.version())).resolve(
                 LICENSE_FILE.getFileName());
@@ -120,7 +123,7 @@
                         expectedLicensePath, LinuxHelper.getPackageName(cmd)));
     }
 
-    private static void verifyLicenseFileInstalledLinux(Path licenseFile) {
+    private static void verifyLicenseFileInstalledRpm(Path licenseFile) {
         Test.assertTrue(Files.isReadable(licenseFile), String.format(
                 "Check license file [%s] is readable", licenseFile));
         try {
@@ -133,6 +136,35 @@
         }
     }
 
+    private static void verifyLicenseFileInstalledDebian(Path licenseFile) {
+        Test.assertTrue(Files.isReadable(licenseFile), String.format(
+                "Check license file [%s] is readable", licenseFile));
+
+        Function<List<String>, List<String>> stripper = (lines) -> Arrays.asList(
+                String.join("\n", lines).stripTrailing().split("\n"));
+
+        try {
+            List<String> actualLines = Files.readAllLines(licenseFile).stream().dropWhile(
+                    line -> !line.startsWith("License:")).collect(
+                            Collectors.toList());
+            // Remove leading `License:` followed by the whitespace from the first text line.
+            actualLines.set(0, actualLines.get(0).split("\\s+", 2)[1]);
+
+            actualLines = stripper.apply(actualLines);
+
+            Test.assertNotEquals(0, String.join("\n", actualLines).length(),
+                    "Check stripped license text is not empty");
+
+            Test.assertTrue(actualLines.equals(
+                    stripper.apply(Files.readAllLines(LICENSE_FILE))),
+                    String.format(
+                            "Check subset of package license file [%s] is a match of the source license file [%s]",
+                            licenseFile, LICENSE_FILE));
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
     private static void verifyLicenseFileNotInstalledLinux(Path licenseFile) {
         Test.assertDirectoryExists(licenseFile.getParent(), false);
     }
--- a/test/jdk/tools/jpackage/share/RuntimePackageTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/share/RuntimePackageTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,7 +21,11 @@
  * questions.
  */
 
+import java.nio.file.Path;
+import java.util.Optional;
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
+import jdk.jpackage.test.JPackageCommand;
 
 /**
  * Test --runtime-image parameter.
@@ -41,21 +45,25 @@
  * @summary jpackage with --runtime-image
  * @library ../helpers
  * @comment Temporary disable for Linux and OSX until functionality implemented
- * @requires (os.family == "windows")
+ * @requires (os.family != "mac")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm -Xmx512m RuntimePackageTest
+ * @run main/othervm/timeout=360 -Xmx512m RuntimePackageTest
  */
 public class RuntimePackageTest {
 
     public static void main(String[] args) {
-        new PackageTest()
-        .addInitializer(cmd -> {
-            cmd.addArguments("--runtime-image", System.getProperty("java.home"));
-            // Remove --input parameter from jpackage command line as we don't
-            // create input directory in the test and jpackage fails
-            // if --input references non existant directory.
-            cmd.setArgumentValue("--input", null);
-        })
-        .run();
+        Test.run(args, () -> {
+            new PackageTest()
+            .addInitializer(cmd -> {
+                cmd.addArguments("--runtime-image", Optional.ofNullable(
+                        JPackageCommand.DEFAULT_RUNTIME_IMAGE).orElse(Path.of(
+                                System.getProperty("java.home"))));
+                // Remove --input parameter from jpackage command line as we don't
+                // create input directory in the test and jpackage fails
+                // if --input references non existant directory.
+                cmd.removeArgument("--input");
+            })
+            .run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/share/SimplePackageTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/share/SimplePackageTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 
 /**
@@ -45,7 +46,12 @@
  */
 public class SimplePackageTest {
 
-    public static void main(String[] args) throws Exception {
-        new PackageTest().configureHelloApp().run();
+    public static void main(String[] args) {
+        Test.run(args, () -> {
+            new PackageTest()
+            .configureHelloApp()
+            .addBundleDesktopIntegrationVerifier(false)
+            .run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/share/manage_packages.sh	Mon Sep 16 19:24:32 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,185 +0,0 @@
-#!/bin/bash
-
-#
-# Script to install/uninstall packages produced by jpackage jtreg
-# tests doing platform specific packaging.
-#
-# The script will install/uninstall all packages from the files
-# found in the current directory or the one specified with command line option.
-#
-# When jtreg jpackage tests are executed with jpackage.test.output
-# Java property set, produced package files (msi, exe, deb, rpm, etc.) will 
-# be saved in the directory specified with this property.
-#
-# Usage example:
-# # Set directory where to save package files from jtreg jpackage tests
-# JTREG_OUTPUT_DIR=/tmp/jpackage_jtreg_packages
-#
-# # Run tests and fill $JTREG_OUTPUT_DIR directory with package files
-# jtreg -Djpackage.test.output=$JTREG_OUTPUT_DIR ...
-#
-# # Install all packages
-# manage_pachages.sh -d $JTREG_OUTPUT_DIR
-#
-# # Uninstall all packages
-# manage_pachages.sh -d $JTREG_OUTPUT_DIR -u
-#
-
-#
-# When using with MSI installers, Cygwin shell from which this script is
-# executed should be started as administrator. Otherwise silent installation 
-# won't work.
-#
-
-# Fail fast
-set -e; set -o pipefail;
-
-
-help_usage ()
-{
-    echo "Usage: `basename $0` [OPTION]"
-    echo "Options:"
-    echo "  -h        - print this message"
-    echo "  -v        - verbose output"
-    echo "  -d <dir>  - path to directory where to look for package files"
-    echo "  -u        - uninstall packages instead of the default install"
-    echo "  -t        - dry run, print commands but don't execute them"
-}
-
-error ()
-{
-  echo "$@" > /dev/stderr
-}
-
-fatal ()
-{
-  error "$@"
-  exit 1
-}
-
-fatal_with_help_usage ()
-{
-  error "$@"
-  help_usage
-  exit 1
-}
-
-
-# Directory where to look for package files.
-package_dir=$PWD
-
-# Script debug.
-verbose=
-
-# Operation mode.
-mode=install
-
-dryrun=
-
-while getopts "vhd:ut" argname; do
-    case "$argname" in
-        v) verbose=yes;;
-        t) dryrun=yes;;
-        u) mode=uninstall;;
-        d) package_dir="$OPTARG";;
-        h) help_usage; exit 0;;
-        ?) help_usage; exit 1;;
-    esac
-done
-shift $(( OPTIND - 1 ))
-
-[ -d "$package_dir" ] || fatal_with_help_usage "Package directory [$package_dir] is not a directory"
-
-[ -z "$verbose" ] || set -x
-
-
-function find_packages_of_type ()
-{
-    # sort output alphabetically
-    find "$package_dir" -maxdepth 1 -type f -name '*.'"$1" | sort
-}
-
-function find_packages ()
-{
-    local package_suffixes=(deb rpm msi exe)
-    for suffix in "${package_suffixes[@]}"; do
-        if [ "$mode" == "uninstall" ]; then
-            packages=$(find_packages_of_type $suffix | tac)
-        else
-            packages=$(find_packages_of_type $suffix)
-        fi
-        if [ -n "$packages" ]; then
-            package_type=$suffix
-            break;
-        fi
-    done
-}
-
-
-# RPM
-install_cmd_rpm ()
-{
-    echo sudo rpm --install "$@"
-}
-uninstall_cmd_rpm ()
-{
-    local package_name=$(rpm -qp --queryformat '%{Name}' "$@")
-    echo sudo rpm -e "$package_name"
-}
-
-# DEB
-install_cmd_deb ()
-{
-    echo sudo dpkg -i "$@"
-}
-uninstall_cmd_deb ()
-{
-    local package_name=$(dpkg-deb -f "$@" Package)
-    echo sudo dpkg -r "$package_name"
-}
-
-# MSI
-install_cmd_msi ()
-{
-    echo msiexec /qn /norestart /i $(cygpath -w "$@")
-}
-uninstall_cmd_msi ()
-{
-    echo msiexec /qn /norestart /x $(cygpath -w "$@")
-}
-
-# EXE
-install_cmd_exe ()
-{
-    echo "$@"
-}
-uninstall_cmd_exe ()
-{
-    error No implemented
-}
-
-
-# Find packages
-packages=
-find_packages
-if [ -z "$packages" ]; then
-    echo "No packages found in $package_dir directory"
-    exit
-fi
-
-# Build list of commands to execute
-declare -a commands
-for p in $packages; do
-    commands[${#commands[@]}]=$(${mode}_cmd_${package_type} "$p")
-done
-
-if [ -z "$dryrun" ]; then
-    # Run commands
-    for cmd in "${commands[@]}"; do
-        echo Running: $cmd
-        $cmd || true;
-    done
-else
-    # Print commands
-    for cmd in "${commands[@]}"; do echo $cmd; done
-fi
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/test_jpackage.sh	Tue Sep 24 13:41:16 2019 -0400
@@ -0,0 +1,68 @@
+#!/bin/bash
+
+#
+# Complete testing of jpackage platform-specific packaging.
+#
+# The script does the following:
+# 1. Create packages.
+# 2. Install created packages.
+# 3. Verifies packages are installed.
+# 4. Uninstall created packages.
+# 5. Verifies packages are uninstalled.
+#
+# For the list of accepted command line arguments see `run_tests.sh` script.
+#
+
+# Fail fast
+set -e; set -o pipefail;
+
+# Script debug
+dry_run=${JPACKAGE_TEST_DRY_RUN}
+
+# Default directory where jpackage should write bundle files
+output_dir=~/jpackage_bundles
+
+
+set_args ()
+{
+  args=()
+  local arg_is_output_dir=
+  local arg_is_mode=
+  local output_dir_set=
+  for arg in "$@"; do
+    if [ "$arg" == "-o" ]; then
+      arg_is_output_dir=yes
+      output_dir_set=yes
+    elif [ "$arg" == "-m" ]; then
+      arg_is_mode=yes
+    continue
+    elif [ -n "$arg_is_output_dir" ]; then
+      arg_is_output_dir=
+      output_dir="$arg"
+    elif [ -n "$arg_is_mode" ]; then
+      arg_is_mode=
+      continue
+    fi
+
+    args+=( "$arg" )
+  done
+  [ -n "$output_dir_set" ] || args=( -o "$output_dir" "${args[@]}" )
+}
+
+
+exec_command ()
+{
+  if [ -n "$dry_run" ]; then
+    echo "$@"
+  else
+    eval "$@"
+  fi
+}
+
+set_args "$@"
+basedir="$(dirname $0)"
+exec_command "$basedir/run_tests.sh" -m create "${args[@]}"
+exec_command "$basedir/manage_packages.sh" -d "$output_dir"
+exec_command "$basedir/run_tests.sh" -m verify-install "${args[@]}"
+exec_command "$basedir/manage_packages.sh" -d "$output_dir" -u
+exec_command "$basedir/run_tests.sh" -m verify-uninstall "${args[@]}"
--- a/test/jdk/tools/jpackage/windows/WinConsoleTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinConsoleTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -37,24 +37,26 @@
  * @library ../helpers
  * @requires (os.family == "windows")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm -Xmx512m WinConsoleTest
+ * @run main/othervm/timeout=360 -Xmx512m WinConsoleTest
  */
 public class WinConsoleTest {
 
-    public static void main(String[] args) throws IOException {
-        JPackageCommand cmd = JPackageCommand.helloAppImage();
-        final Path launcherPath = cmd.appImage().resolve(
-                cmd.launcherPathInAppImage());
+    public static void main(String[] args) {
+        Test.run(args, () -> {
+            JPackageCommand cmd = JPackageCommand.helloAppImage();
+            final Path launcherPath = cmd.appImage().resolve(
+                    cmd.launcherPathInAppImage());
 
-        IOUtils.deleteRecursive(cmd.outputDir().toFile());
-        cmd.execute().assertExitCodeIsZero();
-        HelloApp.executeAndVerifyOutput(launcherPath);
-        checkSubsystem(launcherPath, false);
+            IOUtils.deleteRecursive(cmd.outputDir().toFile());
+            cmd.execute().assertExitCodeIsZero();
+            HelloApp.executeLauncherAndVerifyOutput(cmd);
+            checkSubsystem(launcherPath, false);
 
-        IOUtils.deleteRecursive(cmd.outputDir().toFile());
-        cmd.addArgument("--win-console").execute().assertExitCodeIsZero();
-        HelloApp.executeAndVerifyOutput(launcherPath);
-        checkSubsystem(launcherPath, true);
+            IOUtils.deleteRecursive(cmd.outputDir().toFile());
+            cmd.addArgument("--win-console").execute().assertExitCodeIsZero();
+            HelloApp.executeLauncherAndVerifyOutput(cmd);
+            checkSubsystem(launcherPath, true);
+        });
     }
 
     private static void checkSubsystem(Path path, boolean isConsole) throws
--- a/test/jdk/tools/jpackage/windows/WinDirChooserTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinDirChooserTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 
@@ -38,14 +39,16 @@
  * @library ../helpers
  * @requires (os.family == "windows")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm -Xmx512m WinDirChooserTest
+ * @run main/othervm/timeout=360 -Xmx512m WinDirChooserTest
  */
 
 public class WinDirChooserTest {
     public static void main(String[] args) {
-        new PackageTest()
-        .forTypes(PackageType.WINDOWS)
-        .configureHelloApp()
-        .addInitializer(cmd -> cmd.addArgument("--win-dir-chooser")).run();
+        Test.run(args, () -> {
+            new PackageTest()
+            .forTypes(PackageType.WINDOWS)
+            .configureHelloApp()
+            .addInitializer(cmd -> cmd.addArgument("--win-dir-chooser")).run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/windows/WinMenuGroupTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinMenuGroupTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 
@@ -40,16 +41,18 @@
  * @library ../helpers
  * @requires (os.family == "windows")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm -Xmx512m WinMenuGroupTest
+ * @run main/othervm/timeout=360 -Xmx512m WinMenuGroupTest
  */
 
 public class WinMenuGroupTest {
     public static void main(String[] args) {
-        new PackageTest()
-        .forTypes(PackageType.WINDOWS)
-        .configureHelloApp()
-        .addInitializer(cmd -> cmd.addArguments(
-                "--win-menu", "--win-menu-group", "WinMenuGroupTest_MenuGroup"))
-        .run();
+        Test.run(args, () -> {
+            new PackageTest()
+            .forTypes(PackageType.WINDOWS)
+            .configureHelloApp()
+            .addInitializer(cmd -> cmd.addArguments(
+                    "--win-menu", "--win-menu-group", "WinMenuGroupTest_MenuGroup"))
+            .run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/windows/WinMenuTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinMenuTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 
@@ -37,14 +38,16 @@
  * @library ../helpers
  * @requires (os.family == "windows")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm -Xmx512m WinMenuTest
+ * @run main/othervm/timeout=360 -Xmx512m WinMenuTest
  */
 
 public class WinMenuTest {
     public static void main(String[] args) {
-        new PackageTest()
-        .forTypes(PackageType.WINDOWS)
-        .configureHelloApp()
-        .addInitializer(cmd -> cmd.addArgument("--win-menu")).run();
+        Test.run(args, () -> {
+            new PackageTest()
+            .forTypes(PackageType.WINDOWS)
+            .configureHelloApp()
+            .addInitializer(cmd -> cmd.addArgument("--win-menu")).run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/windows/WinPerUserInstallTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinPerUserInstallTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 
@@ -39,18 +40,20 @@
  * @library ../helpers
  * @requires (os.family == "windows")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm -Xmx512m WinPerUserInstallTest
+ * @run main/othervm/timeout=360 -Xmx512m WinPerUserInstallTest
  */
 
 public class WinPerUserInstallTest {
     public static void main(String[] args) {
-        new PackageTest()
-        .forTypes(PackageType.WINDOWS)
-        .configureHelloApp()
-        .addInitializer(cmd -> cmd.addArguments(
-                "--win-menu",
-                "--win-menu-group", "WinPerUserInstallTest_MenuGroup",
-                "--win-per-user-install"))
-        .run();
+        Test.run(args, () -> {
+            new PackageTest()
+            .forTypes(PackageType.WINDOWS)
+            .configureHelloApp()
+            .addInitializer(cmd -> cmd.addArguments(
+                    "--win-menu",
+                    "--win-menu-group", "WinPerUserInstallTest_MenuGroup",
+                    "--win-per-user-install"))
+            .run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/windows/WinShortcutTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinShortcutTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 
@@ -38,15 +39,17 @@
  * @library ../helpers
  * @requires (os.family == "windows")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm -Xmx512m WinShortcutTest
+ * @run main/othervm/timeout=360 -Xmx512m WinShortcutTest
  */
 
 public class WinShortcutTest {
     public static void main(String[] args) {
-        new PackageTest()
-        .forTypes(PackageType.WINDOWS)
-        .configureHelloApp()
-        .addInitializer(cmd -> cmd.addArgument("--win-shortcut"))
-        .run();
+        Test.run(args, () -> {
+            new PackageTest()
+            .forTypes(PackageType.WINDOWS)
+            .configureHelloApp()
+            .addInitializer(cmd -> cmd.addArgument("--win-shortcut"))
+            .run();
+        });
     }
 }
--- a/test/jdk/tools/jpackage/windows/WinUpgradeUUIDTest.java	Mon Sep 16 19:24:32 2019 -0400
+++ b/test/jdk/tools/jpackage/windows/WinUpgradeUUIDTest.java	Tue Sep 24 13:41:16 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import jdk.jpackage.test.Test;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
 
@@ -41,29 +42,31 @@
  * @library ../helpers
  * @requires (os.family == "windows")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm -Xmx512m WinUpgradeUUIDTest
+ * @run main/othervm/timeout=360 -Xmx512m WinUpgradeUUIDTest
  */
 
 public class WinUpgradeUUIDTest {
     public static void main(String[] args) {
-        PackageTest test = init();
-        if (test.getAction() != PackageTest.Action.VERIFY_INSTALLED) {
-            test.run();
-        }
+        Test.run(args, () -> {
+            PackageTest test = init();
+            if (test.getAction() != PackageTest.Action.VERIFY_INSTALL) {
+                test.run();
+            }
 
-        test = init();
-        test.addInitializer(cmd -> {
-            cmd.setArgumentValue("--app-version", "2.0");
-            cmd.setArgumentValue("--arguments", "bar");
+            test = init();
+            test.addInitializer(cmd -> {
+                cmd.setArgumentValue("--app-version", "2.0");
+                cmd.setArgumentValue("--arguments", "bar");
+            });
+            test.run();
         });
-        test.run();
     }
 
     private static PackageTest init() {
         return new PackageTest()
-        .forTypes(PackageType.WINDOWS)
-        .configureHelloApp()
-        .addInitializer(cmd -> cmd.addArguments("--win-upgrade-uuid",
-                "F0B18E75-52AD-41A2-BC86-6BE4FCD50BEB"));
+            .forTypes(PackageType.WINDOWS)
+            .configureHelloApp()
+            .addInitializer(cmd -> cmd.addArguments("--win-upgrade-uuid",
+                    "F0B18E75-52AD-41A2-BC86-6BE4FCD50BEB"));
     }
 }