8223325: Improve wix sources generated by jpackage JDK-8200758-branch
authorherrick
Fri, 18 Oct 2019 14:14:37 -0400
branchJDK-8200758-branch
changeset 58696 61c44899b4eb
parent 58695 64adf683bc7b
child 58761 88e2753a2334
8223325: Improve wix sources generated by jpackage Submitted-by: asemenyuk Reviewed-by: aherrick, almatvee
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java
src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/DottedVersion.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociation.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/ToolValidator.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/VersionExtractor.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsBundlerParam.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixSourcesBuilder.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/.Executor.java.swp
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/JarBuilder.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaAppDesc.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java
test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java
test/jdk/tools/jpackage/junit/jdk/jpackage/internal/CompareDottedVersionTest.java
test/jdk/tools/jpackage/junit/jdk/jpackage/internal/DottedVersionTest.java
test/jdk/tools/jpackage/junit/jdk/jpackage/internal/InvalidDottedVersionTest.java
test/jdk/tools/jpackage/junit/jdk/jpackage/internal/PathGroupTest.java
test/jdk/tools/jpackage/share/FileAssociationsTest.java
test/jdk/tools/jpackage/share/IconTest.java
test/jdk/tools/jpackage/share/RuntimePackageTest.java
test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java	Fri Oct 18 14:14:37 2019 -0400
@@ -31,7 +31,6 @@
 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.ICON_PNG;
@@ -353,15 +352,9 @@
     }
 
     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();
+        IOUtils.createXml(mimeInfoFile.srcPath(), xml -> {
             xml.writeStartElement("mime-info");
-            xml.writeNamespace("xmlns",
+            xml.writeDefaultNamespace(
                     "http://www.freedesktop.org/standards/shared-mime-info");
 
             for (var assoc : associations) {
@@ -369,14 +362,7 @@
             }
 
             xml.writeEndElement();
-            xml.writeEndDocument();
-            xml.flush();
-            xml.close();
-
-        } catch (XMLStreamException ex) {
-            Log.verbose(ex);
-            throw new IOException(ex);
-        }
+        });
     }
 
     private Map<String, Path> createFileAssociationIconFiles() throws
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java	Fri Oct 18 14:14:37 2019 -0400
@@ -26,7 +26,6 @@
 package jdk.jpackage.internal;
 
 import java.io.*;
-import java.nio.charset.StandardCharsets;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -140,9 +139,7 @@
                 try {
                     String licenseFile = LICENSE_FILE.fetchFrom(params);
                     if (licenseFile != null) {
-                        return Files.lines(Path.of(licenseFile),
-                                StandardCharsets.UTF_8).collect(
-                                        Collectors.joining("\n"));
+                        return Files.readString(Path.of(licenseFile));
                     }
                 } catch (IOException e) {
                     Log.verbose(e);
--- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java	Fri Oct 18 14:14:37 2019 -0400
@@ -25,12 +25,7 @@
 
 package jdk.jpackage.internal;
 
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.io.Writer;
+import java.io.*;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.file.Files;
@@ -42,9 +37,6 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.ResourceBundle;
-import javax.xml.stream.XMLOutputFactory;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.XMLStreamWriter;
 
 import static jdk.jpackage.internal.StandardBundlerParam.*;
 import static jdk.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEYCHAIN;
@@ -155,7 +147,7 @@
                 return createPKG(params, outdir, appImageDir);
             }
             return null;
-        } catch (IOException|URISyntaxException ex) {
+        } catch (IOException ex) {
             Log.verbose(ex);
             throw new PackagerException(ex);
         }
@@ -219,25 +211,19 @@
         getScripts_PostinstallFile(params).setExecutable(true, false);
     }
 
-    private String URLEncoding(String pkgName) throws URISyntaxException {
+    private static String URLEncoding(String pkgName) throws URISyntaxException {
         URI uri = new URI(null, null, pkgName, null);
         return uri.toASCIIString();
     }
 
     private void prepareDistributionXMLFile(Map<String, ? super Object> params)
-            throws IOException, URISyntaxException {
+            throws IOException {
         File f = getConfig_DistributionXMLFile(params);
 
         Log.verbose(MessageFormat.format(I18N.getString(
                 "message.preparing-distribution-dist"), f.getAbsolutePath()));
 
-        XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
-
-        try (Writer w = new OutputStreamWriter(new FileOutputStream(f), "UTF-8")) {
-            XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w);
-
-            xml.writeStartDocument("UTF-8", "1.0");
-
+        IOUtils.createXml(f.toPath(), xml -> {
             xml.writeStartElement("installer-gui-script");
             xml.writeAttribute("minSpecVersion", "1");
 
@@ -303,24 +289,20 @@
             xml.writeAttribute("id", appId);
             xml.writeAttribute("version", VERSION.fetchFrom(params));
             xml.writeAttribute("onConclusion", "none");
-            xml.writeCharacters(URLEncoding(
-                    getPackages_AppPackage(params).getName()));
+            try {
+                xml.writeCharacters(URLEncoding(
+                        getPackages_AppPackage(params).getName()));
+            } catch (URISyntaxException ex) {
+                throw new IOException(ex);
+            }
             xml.writeEndElement(); // </pkg-ref>
 
             xml.writeEndElement(); // </installer-gui-script>
-
-            xml.writeEndDocument();
-            xml.flush();
-            xml.close();
-
-        } catch (XMLStreamException ex) {
-            Log.verbose(ex);
-            throw new IOException(ex);
-        }
+        });
     }
 
     private boolean prepareConfigFiles(Map<String, ? super Object> params)
-            throws IOException, URISyntaxException {
+            throws IOException {
 
         createResource(DEFAULT_BACKGROUND_IMAGE, params)
                 .setCategory(I18N.getString("resource.pkg-background-image"))
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java	Fri Oct 18 14:14:37 2019 -0400
@@ -1,218 +1,233 @@
-/*
- * 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.BufferedWriter;
-import java.io.FileInputStream;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.Writer;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.stream.XMLOutputFactory;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.XMLStreamWriter;
-import javax.xml.xpath.XPath;
-import javax.xml.xpath.XPathConstants;
-import javax.xml.xpath.XPathExpressionException;
-import javax.xml.xpath.XPathFactory;
-import org.w3c.dom.Document;
-import org.w3c.dom.NodeList;
-import org.xml.sax.SAXException;
-
-import static jdk.jpackage.internal.StandardBundlerParam.*;
-
-class AppImageFile {
-
-    // These values will be loaded from AppImage xml file.
-    private final String creatorVersion;
-    private final String creatorPlatform;
-    private final String launcherName;
-    private final List<String> addLauncherNames;
-    
-    final static String XML_FILENAME = ".jpackage.xml";
-    
-    private final static Map<Platform, String> PLATFORM_LABELS = Map.of(
-            Platform.LINUX, "linux", Platform.WINDOWS, "windows", Platform.MAC,
-            "macOS");
-    
-
-    private AppImageFile() {
-        this(null, null, null, null);
-    }
-    
-    private AppImageFile(String launcherName, List<String> addLauncherNames,
-            String creatorVersion, String creatorPlatform) {
-        this.launcherName = launcherName;
-        this.addLauncherNames = addLauncherNames;
-        this.creatorVersion = creatorVersion;
-        this.creatorPlatform = creatorPlatform;
-    }
-
-    /**
-     * Would return null to indicate stored command line is invalid.
-     */
-    List<String> getAddLauncherNames() {
-        return addLauncherNames;
-    }
-
-    String getLauncherName() {
-        return launcherName;
-    }
-    
-    void verifyCompatible() throws ConfigException {
-        // Just do nohing for now.
-    }
-
-    static void save(Path appImage, Map<String, Object> params)
-            throws IOException {
-        Path xmlFile = appImage.resolve(XML_FILENAME);
-        XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
-
-        try (Writer w = new BufferedWriter(new FileWriter(xmlFile.toFile()))) {
-            XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w);
-
-            xml.writeStartDocument();
-            xml.writeStartElement("jpackage-state");
-            xml.writeAttribute("version", getVersion());
-            xml.writeAttribute("platform", getPlatform());
-            
-            xml.writeStartElement("main-launcher");
-            xml.writeCharacters(APP_NAME.fetchFrom(params));
-            xml.writeEndElement();
-            
-            List<Map<String, ? super Object>> addLaunchers =
-                ADD_LAUNCHERS.fetchFrom(params);
-
-            for (int i = 0; i < addLaunchers.size(); i++) {
-                Map<String, ? super Object> sl = addLaunchers.get(i);
-                xml.writeStartElement("add-launcher");
-                xml.writeCharacters(APP_NAME.fetchFrom(sl));
-                xml.writeEndElement();
-            }
-            
-            xml.writeEndElement();
-            xml.writeEndDocument();
-            xml.flush();
-            xml.close();
-
-        } catch (XMLStreamException ex) {
-            Log.verbose(ex);
-            throw new IOException(ex);
-        }
-    }
-
-    static AppImageFile load(Path appImageDir) throws IOException {
-        try {
-            Path path = appImageDir.resolve(XML_FILENAME);
-            DocumentBuilderFactory dbf =
-                    DocumentBuilderFactory.newDefaultInstance();
-            dbf.setFeature(
-                   "http://apache.org/xml/features/nonvalidating/load-external-dtd",
-                    false);
-            DocumentBuilder b = dbf.newDocumentBuilder();
-            Document doc = b.parse(new FileInputStream(path.toFile()));
-
-            XPath xPath = XPathFactory.newInstance().newXPath();
-            
-            String mainLauncher = xpathQueryNullable(xPath,
-                    "/jpackage-state/main-launcher/text()", doc);
-            if (mainLauncher == null) {
-                // No main launcher, this is fatal.
-                return new AppImageFile();
-            }
-
-            List<String> addLaunchers = new ArrayList<String>();
-
-            String platform = xpathQueryNullable(xPath,
-                    "/jpackage-state/@platform", doc);
-
-            String version = xpathQueryNullable(xPath,
-                    "/jpackage-state/@version", doc);
-            
-            NodeList launcherNameNodes = (NodeList) xPath.evaluate(
-                    "/jpackage-state/add-launcher/text()", doc,
-                    XPathConstants.NODESET);
-
-            for (int i = 0; i != launcherNameNodes.getLength(); i++) {
-                addLaunchers.add(launcherNameNodes.item(i).getNodeValue());
-            }
-            
-            AppImageFile file = new AppImageFile(
-                    mainLauncher, addLaunchers, version, platform);
-            if (!file.isValid()) {
-                file = new AppImageFile();
-            }
-            return file;       
-        } catch (ParserConfigurationException | SAXException ex) {
-            // Let caller sort this out
-            throw new IOException(ex);
-        } catch (XPathExpressionException ex) {
-            // This should never happen as XPath expressions should be correct
-            throw new RuntimeException(ex);
-        }
-    }
-    
-    private static String xpathQueryNullable(XPath xPath, String xpathExpr,
-            Document xml) throws XPathExpressionException {
-        NodeList nodes = (NodeList) xPath.evaluate(xpathExpr, xml,
-                XPathConstants.NODESET);
-        if (nodes != null && nodes.getLength() > 0) {
-            return nodes.item(0).getNodeValue();
-        }
-        return null;
-    }
-
-    private static String getVersion() {
-        return System.getProperty("java.version");
-    }
-    
-    private static String getPlatform() {
-        return PLATFORM_LABELS.get(Platform.getPlatform());
-    }
-    
-    private boolean isValid() {
-        if (launcherName == null || launcherName.length() == 0 ||
-            addLauncherNames.indexOf("") != -1) {
-            // Some launchers have empty names. This is invalid.
-            return false;
-        }
-        
-        // Add more validation.
-        
-        return true;
-    }
-    
-}
+/*
+ * 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.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import static jdk.jpackage.internal.StandardBundlerParam.*;
+
+public class AppImageFile {
+
+    // These values will be loaded from AppImage xml file.
+    private final String creatorVersion;
+    private final String creatorPlatform;
+    private final String launcherName;
+    private final List<String> addLauncherNames;
+
+    public final static String FILENAME = ".jpackage.xml";
+
+    private final static Map<Platform, String> PLATFORM_LABELS = Map.of(
+            Platform.LINUX, "linux", Platform.WINDOWS, "windows", Platform.MAC,
+            "macOS");
+
+
+    private AppImageFile() {
+        this(null, null, null, null);
+    }
+
+    private AppImageFile(String launcherName, List<String> addLauncherNames,
+            String creatorVersion, String creatorPlatform) {
+        this.launcherName = launcherName;
+        this.addLauncherNames = addLauncherNames;
+        this.creatorVersion = creatorVersion;
+        this.creatorPlatform = creatorPlatform;
+    }
+
+    /**
+     * Returns list of additional launchers configured for the application.
+     * Each item in the list is not null or empty string.
+     * Returns empty list for application without additional launchers.
+     */
+    List<String> getAddLauncherNames() {
+        return addLauncherNames;
+    }
+
+    /**
+     * Returns main application launcher name. Never returns null or empty value.
+     */
+    String getLauncherName() {
+        return launcherName;
+    }
+
+    void verifyCompatible() throws ConfigException {
+        // Just do nothing for now.
+    }
+
+    /**
+     * Saves file with application image info in application image.
+     * @param appImageDir - path to application image
+     * @throws IOException
+     */
+    static void save(Path appImage, Map<String, Object> params)
+            throws IOException {
+        IOUtils.createXml(appImage.resolve(FILENAME), xml -> {
+            xml.writeStartElement("jpackage-state");
+            xml.writeAttribute("version", getVersion());
+            xml.writeAttribute("platform", getPlatform());
+
+            xml.writeStartElement("main-launcher");
+            xml.writeCharacters(APP_NAME.fetchFrom(params));
+            xml.writeEndElement();
+
+            List<Map<String, ? super Object>> addLaunchers =
+                ADD_LAUNCHERS.fetchFrom(params);
+
+            for (int i = 0; i < addLaunchers.size(); i++) {
+                Map<String, ? super Object> sl = addLaunchers.get(i);
+                xml.writeStartElement("add-launcher");
+                xml.writeCharacters(APP_NAME.fetchFrom(sl));
+                xml.writeEndElement();
+            }
+        });
+    }
+
+    /**
+     * Loads application image info from application image.
+     * @param appImageDir - path to application image
+     * @return valid info about application image or null
+     * @throws IOException
+     */
+    static AppImageFile load(Path appImageDir) throws IOException {
+        try {
+            Path path = appImageDir.resolve(FILENAME);
+            DocumentBuilderFactory dbf =
+                    DocumentBuilderFactory.newDefaultInstance();
+            dbf.setFeature(
+                   "http://apache.org/xml/features/nonvalidating/load-external-dtd",
+                    false);
+            DocumentBuilder b = dbf.newDocumentBuilder();
+            Document doc = b.parse(new FileInputStream(path.toFile()));
+
+            XPath xPath = XPathFactory.newInstance().newXPath();
+
+            String mainLauncher = xpathQueryNullable(xPath,
+                    "/jpackage-state/main-launcher/text()", doc);
+            if (mainLauncher == null) {
+                // No main launcher, this is fatal.
+                return new AppImageFile();
+            }
+
+            List<String> addLaunchers = new ArrayList<String>();
+
+            String platform = xpathQueryNullable(xPath,
+                    "/jpackage-state/@platform", doc);
+
+            String version = xpathQueryNullable(xPath,
+                    "/jpackage-state/@version", doc);
+
+            NodeList launcherNameNodes = (NodeList) xPath.evaluate(
+                    "/jpackage-state/add-launcher/text()", doc,
+                    XPathConstants.NODESET);
+
+            for (int i = 0; i != launcherNameNodes.getLength(); i++) {
+                addLaunchers.add(launcherNameNodes.item(i).getNodeValue());
+            }
+
+            AppImageFile file = new AppImageFile(
+                    mainLauncher, addLaunchers, version, platform);
+            if (!file.isValid()) {
+                file = new AppImageFile();
+            }
+            return file;
+        } catch (ParserConfigurationException | SAXException ex) {
+            // Let caller sort this out
+            throw new IOException(ex);
+        } catch (XPathExpressionException ex) {
+            // This should never happen as XPath expressions should be correct
+            throw new RuntimeException(ex);
+        }
+    }
+
+    /**
+     * Returns list of launcher names configured for the application.
+     * The first item in the returned list is man launcher name.
+     * Following items in the list are names of additional launchers.
+     */
+    static List<String> getLauncherNames(Path appImageDir,
+            Map<String, ? super Object> params) {
+        List<String> launchers = new ArrayList<>();
+        try {
+            AppImageFile appImageInfo = AppImageFile.load(appImageDir);
+            if (appImageInfo != null) {
+                launchers.add(appImageInfo.getLauncherName());
+                launchers.addAll(appImageInfo.getAddLauncherNames());
+                return launchers;
+            }
+        } catch (IOException ioe) {
+            Log.verbose(ioe);
+        }
+
+        launchers.add(APP_NAME.fetchFrom(params));
+        ADD_LAUNCHERS.fetchFrom(params).stream().map(APP_NAME::fetchFrom).forEach(
+                launchers::add);
+        return launchers;
+    }
+
+    private static String xpathQueryNullable(XPath xPath, String xpathExpr,
+            Document xml) throws XPathExpressionException {
+        NodeList nodes = (NodeList) xPath.evaluate(xpathExpr, xml,
+                XPathConstants.NODESET);
+        if (nodes != null && nodes.getLength() > 0) {
+            return nodes.item(0).getNodeValue();
+        }
+        return null;
+    }
+
+    private static String getVersion() {
+        return System.getProperty("java.version");
+    }
+
+    private static String getPlatform() {
+        return PLATFORM_LABELS.get(Platform.getPlatform());
+    }
+
+    private boolean isValid() {
+        if (launcherName == null || launcherName.length() == 0 ||
+            addLauncherNames.indexOf("") != -1) {
+            // Some launchers have empty names. This is invalid.
+            return false;
+        }
+
+        // Add more validation.
+
+        return true;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DottedVersion.java	Fri Oct 18 14:14:37 2019 -0400
@@ -0,0 +1,101 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Dotted numeric version string.
+ * E.g.: 1.0.37, 10, 0.5
+ */
+class DottedVersion implements Comparable<String> {
+
+    public DottedVersion(String version) {
+        components = parseVersionString(version);
+        value = version;
+    }
+
+    @Override
+    public int compareTo(String o) {
+        int result = 0;
+        int[] otherComponents = parseVersionString(o);
+        for (int i = 0; i < Math.min(components.length, otherComponents.length)
+                && result == 0; ++i) {
+            result = components[i] - otherComponents[i];
+        }
+
+        if (result == 0) {
+            result = components.length - otherComponents.length;
+        }
+
+        return result;
+    }
+
+    private static int[] parseVersionString(String version) {
+        Objects.requireNonNull(version);
+        if (version.isEmpty()) {
+            throw new IllegalArgumentException("Version may not be empty string");
+        }
+
+        int lastNotZeroIdx = -1;
+        List<Integer> components = new ArrayList<>();
+        for (var component : version.split("\\.", -1)) {
+            if (component.isEmpty()) {
+                throw new IllegalArgumentException(String.format(
+                        "Version [%s] contains a zero lenght component", version));
+            }
+
+            int num = Integer.parseInt(component);
+            if (num < 0) {
+                throw new IllegalArgumentException(String.format(
+                        "Version [%s] contains invalid component [%s]", version,
+                        component));
+            }
+
+            if (num != 0) {
+                lastNotZeroIdx = components.size();
+            }
+            components.add(num);
+        }
+
+        if (lastNotZeroIdx + 1 != components.size()) {
+            // Strip trailing zeros.
+            components = components.subList(0, lastNotZeroIdx + 1);
+        }
+
+        return components.stream().mapToInt(Integer::intValue).toArray();
+    }
+
+    @Override
+    public String toString() {
+        return value;
+    }
+
+    final private int[] components;
+    final private String value;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/FileAssociation.java	Fri Oct 18 14:14:37 2019 -0400
@@ -0,0 +1,70 @@
+/*
+ * 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.File;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.stream.Collectors;
+import static jdk.jpackage.internal.StandardBundlerParam.*;
+
+final class FileAssociation {
+    void verify() {
+        if (extensions.isEmpty()) {
+            Log.error(I18N.getString(
+                    "message.creating-association-with-null-extension"));
+        }
+    }
+
+    static List<FileAssociation> fetchFrom(Map<String, ? super Object> params) {
+        String launcherName = APP_NAME.fetchFrom(params);
+
+        return FILE_ASSOCIATIONS.fetchFrom(params).stream().filter(
+                Objects::nonNull).map(fa -> {
+                    FileAssociation assoc = new FileAssociation();
+
+                    assoc.launcherPath = Path.of(launcherName);
+                    assoc.description = FA_DESCRIPTION.fetchFrom(fa);
+                    assoc.extensions = Optional.ofNullable(
+                            FA_EXTENSIONS.fetchFrom(fa)).orElse(Collections.emptyList());
+                    assoc.mimeTypes = Optional.ofNullable(
+                            FA_CONTENT_TYPE.fetchFrom(fa)).orElse(Collections.emptyList());
+
+                    File icon = FA_ICON.fetchFrom(fa);
+                    if (icon != null) {
+                        assoc.iconPath = icon.toPath();
+                    }
+
+                    return assoc;
+                }).collect(Collectors.toList());
+    }
+
+    Path launcherPath;
+    Path iconPath;
+    List<String> mimeTypes;
+    List<String> extensions;
+    String description;
+}
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java	Fri Oct 18 14:14:37 2019 -0400
@@ -26,14 +26,19 @@
 package jdk.jpackage.internal;
 
 import java.io.*;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
 import java.nio.channels.FileChannel;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
 
 /**
  * IOUtils
@@ -221,4 +226,105 @@
                     file.getAbsolutePath());
         }
     }
+
+    public static Path replaceSuffix(Path path, String suffix) {
+        Path parent = path.getParent();
+        String filename = path.getFileName().toString().replaceAll("\\.[^.]*$", "")
+                + Optional.ofNullable(suffix).orElse("");
+        return parent != null ? parent.resolve(filename) : Path.of(filename);
+    }
+
+    public static Path addSuffix(Path path, String suffix) {
+        Path parent = path.getParent();
+        String filename = path.getFileName().toString() + suffix;
+        return parent != null ? parent.resolve(filename) : Path.of(filename);
+    }
+
+    @FunctionalInterface
+    public static interface XmlConsumer {
+        void accept(XMLStreamWriter xml) throws IOException, XMLStreamException;
+    }
+
+    public static void createXml(Path dstFile, XmlConsumer xmlConsumer) throws
+            IOException {
+        XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
+        try (Writer w = new BufferedWriter(new FileWriter(dstFile.toFile()))) {
+            // Wrap with pretty print proxy
+            XMLStreamWriter xml = (XMLStreamWriter) Proxy.newProxyInstance(
+                    XMLStreamWriter.class.getClassLoader(), new Class<?>[]{
+                XMLStreamWriter.class}, new PrettyPrintHandler(
+                    xmlFactory.createXMLStreamWriter(w)));
+
+            xml.writeStartDocument();
+            xmlConsumer.accept(xml);
+            xml.writeEndDocument();
+            xml.flush();
+            xml.close();
+        } catch (XMLStreamException ex) {
+            throw new IOException(ex);
+        } catch (IOException ex) {
+            throw ex;
+        }
+    }
+
+    private static class PrettyPrintHandler implements InvocationHandler {
+
+        PrettyPrintHandler(XMLStreamWriter target) {
+            this.target = target;
+        }
+
+        @Override
+        public Object invoke(Object proxy, Method method, Object[] args) throws
+                Throwable {
+            switch (method.getName()) {
+                case "writeStartElement":
+                    // update state of parent node
+                    if (depth > 0) {
+                        hasChildElement.put(depth - 1, true);
+                    }
+                    // reset state of current node
+                    hasChildElement.put(depth, false);
+                    // indent for current depth
+                    target.writeCharacters(EOL);
+                    target.writeCharacters(repeat(depth, INDENT));
+                    depth++;
+                    break;
+                case "writeEndElement":
+                    depth--;
+                    if (hasChildElement.get(depth) == true) {
+                        target.writeCharacters(EOL);
+                        target.writeCharacters(repeat(depth, INDENT));
+                    }
+                    break;
+                case "writeProcessingInstruction":
+                case "writeEmptyElement":
+                    // update state of parent node
+                    if (depth > 0) {
+                        hasChildElement.put(depth - 1, true);
+                    }
+                    // indent for current depth
+                    target.writeCharacters(EOL);
+                    target.writeCharacters(repeat(depth, INDENT));
+                    break;
+                default:
+                    break;
+            }
+            method.invoke(target, args);
+            return null;
+        }
+
+        private static String repeat(int d, String s) {
+            StringBuilder sb = new StringBuilder();
+            while (d-- > 0) {
+                sb.append(s);
+            }
+            return sb.toString();
+        }
+
+        private final XMLStreamWriter target;
+        private int depth = 0;
+        private final Map<Integer, Boolean> hasChildElement = new HashMap<>();
+        private static final String INDENT = "  ";
+        private static final String EOL = "\n";
+    }
 }
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PathGroup.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PathGroup.java	Fri Oct 18 14:14:37 2019 -0400
@@ -45,13 +45,24 @@
  */
 final class PathGroup {
     PathGroup(Map<Object, Path> paths) {
-        entries = Collections.unmodifiableMap(paths);
+        entries = new HashMap<>(paths);
     }
 
     Path getPath(Object id) {
+        if (id == null) {
+            throw new NullPointerException();
+        }
         return entries.get(id);
     }
 
+    void setPath(Object id, Path path) {
+        if (path != null) {
+            entries.put(id, path);
+        } else {
+            entries.remove(id);
+        }
+    }
+
     /**
      * All configured entries.
      */
@@ -98,11 +109,15 @@
     }
 
     void copy(PathGroup dst) throws IOException {
-        copy(this, dst, false);
+        copy(this, dst, null, false);
     }
 
     void move(PathGroup dst) throws IOException {
-        copy(this, dst, true);
+        copy(this, dst, null, true);
+    }
+
+    void transform(PathGroup dst, TransformHandler handler) throws IOException {
+        copy(this, dst, handler, false);
     }
 
     static interface Facade<T> {
@@ -129,18 +144,57 @@
         default void move(Facade<T> dst) throws IOException {
             pathGroup().move(dst.pathGroup());
         }
+
+        default void transform(Facade<T> dst, TransformHandler handler) throws
+                IOException {
+            pathGroup().transform(dst.pathGroup(), handler);
+        }
+    }
+
+    static interface TransformHandler {
+        public void copyFile(Path src, Path dst) throws IOException;
+        public void createDirectory(Path dir) throws IOException;
     }
 
-    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(PathGroup src, PathGroup dst,
+            TransformHandler handler, boolean move) throws IOException {
+        List<Map.Entry<Path, Path>> copyItems = new ArrayList<>();
+        List<Path> excludeItems = new ArrayList<>();
+
+        for (var id: src.entries.keySet()) {
+            Path srcPath = src.entries.get(id);
+            if (dst.entries.containsKey(id)) {
+                copyItems.add(Map.entry(srcPath, dst.entries.get(id)));
+            } else {
+                excludeItems.add(srcPath);
+            }
+        }
+
+        copy(move, copyItems, excludeItems, handler);
     }
 
-    private static void copy(boolean move, List<Map.Entry<Path, Path>> entries)
-            throws IOException {
+    private static void copy(boolean move, List<Map.Entry<Path, Path>> entries,
+            List<Path> excludePaths, TransformHandler handler) throws
+            IOException {
+
+        if (handler == null) {
+            handler = new TransformHandler() {
+                @Override
+                public void copyFile(Path src, Path dst) throws IOException {
+                    Files.createDirectories(dst.getParent());
+                    if (move) {
+                        Files.move(src, dst);
+                    } else {
+                        Files.copy(src, dst);
+                    }
+                }
+
+                @Override
+                public void createDirectory(Path dir) throws IOException {
+                    Files.createDirectories(dir);
+                }
+            };
+        }
 
         // destination -> source file mapping
         Map<Path, Path> actions = new HashMap<>();
@@ -149,12 +203,11 @@
             Path dst = action.getValue();
             if (src.toFile().isDirectory()) {
                try (Stream<Path> stream = Files.walk(src)) {
-                   stream.forEach(path -> actions.put(dst.resolve(
-                           src.relativize(path)).toAbsolutePath().normalize(),
-                           path));
+                   stream.sequential().forEach(path -> actions.put(dst.resolve(
+                            src.relativize(path)).normalize(), path));
                }
             } else {
-                actions.put(dst.toAbsolutePath().normalize(), src);
+                actions.put(dst.normalize(), src);
             }
         }
 
@@ -162,19 +215,18 @@
             Path dst = action.getKey();
             Path src = action.getValue();
 
+            if (excludePaths.stream().anyMatch(src::startsWith)) {
+                continue;
+            }
+
             if (src.equals(dst) || !src.toFile().exists()) {
                 continue;
             }
 
             if (src.toFile().isDirectory()) {
-                Files.createDirectories(dst);
+                handler.createDirectory(dst);
             } else {
-                Files.createDirectories(dst.getParent());
-                if (move) {
-                    Files.move(src, dst);
-                } else {
-                    Files.copy(src, dst);
-                }
+                handler.copyFile(src, dst);
             }
         }
 
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java	Fri Oct 18 14:14:37 2019 -0400
@@ -25,23 +25,32 @@
 package jdk.jpackage.internal;
 
 import java.io.IOException;
+import java.nio.file.Path;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.stream.Stream;
 
 
 public final class ToolValidator {
 
-    ToolValidator(String name) {
-        this.name = name;
+    ToolValidator(String tool) {
+        this(Path.of(tool));
+    }
+
+    ToolValidator(Path toolPath) {
+        this.toolPath = toolPath;
         args = new ArrayList<>();
 
         if (Platform.getPlatform() == Platform.LINUX) {
             setCommandLine("--version");
         }
+
+        setToolNotFoundErrorHandler(null);
+        setToolOldVersionErrorHandler(null);
     }
 
     ToolValidator setCommandLine(String... args) {
@@ -59,10 +68,23 @@
         return this;
     }
 
+    ToolValidator setToolNotFoundErrorHandler(
+            BiFunction<String, IOException, ConfigException> v) {
+        toolNotFoundErrorHandler = v;
+        return this;
+    }
+
+    ToolValidator setToolOldVersionErrorHandler(BiFunction<String, String, ConfigException> v) {
+        toolOldVersionErrorHandler = v;
+        return this;
+    }
+
     ConfigException validate() {
         List<String> cmdline = new ArrayList<>();
-        cmdline.add(name);
+        cmdline.add(toolPath.toString());
         cmdline.addAll(args);
+
+        String name = toolPath.getFileName().toString();
         try {
             ProcessBuilder pb = new ProcessBuilder(cmdline);
             AtomicBoolean canUseTool = new AtomicBoolean();
@@ -71,16 +93,20 @@
                 canUseTool.setPlain(true);
             }
 
+            String[] version = new String[1];
             Executor.of(pb).setOutputConsumer(lines -> {
                 if (versionParser != null && minimalVersion != null) {
-                    String version = versionParser.apply(lines);
-                    if (minimalVersion.compareTo(version) < 0) {
+                    version[0] = versionParser.apply(lines);
+                    if (minimalVersion.compareTo(version[0]) < 0) {
                         canUseTool.setPlain(true);
                     }
                 }
             }).execute();
 
             if (!canUseTool.getPlain()) {
+                if (toolOldVersionErrorHandler != null) {
+                    return toolOldVersionErrorHandler.apply(name, version[0]);
+                }
                 return new ConfigException(MessageFormat.format(I18N.getString(
                         "error.tool-old-version"), name, minimalVersion),
                         MessageFormat.format(I18N.getString(
@@ -88,6 +114,9 @@
                                 minimalVersion));
             }
         } catch (IOException e) {
+            if (toolNotFoundErrorHandler != null) {
+                return toolNotFoundErrorHandler.apply(name, e);
+            }
             return new ConfigException(MessageFormat.format(I18N.getString(
                     "error.tool-not-found"), name, e.getMessage()),
                     MessageFormat.format(I18N.getString(
@@ -98,8 +127,10 @@
         return null;
     }
 
-    private final String name;
+    private final Path toolPath;
     private List<String> args;
     private String minimalVersion;
     private Function<Stream<String>, String> versionParser;
+    private BiFunction<String, IOException, ConfigException> toolNotFoundErrorHandler;
+    private BiFunction<String, String, ConfigException> toolOldVersionErrorHandler;
 }
--- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/VersionExtractor.java	Fri Oct 18 11:00:57 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,91 +0,0 @@
-/*
- * 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.ByteArrayOutputStream;
-import java.io.PrintStream;
-import java.text.MessageFormat;
-import java.util.ResourceBundle;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class VersionExtractor extends PrintStream {
-
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-            "jdk.jpackage.internal.resources.WinResources");
-
-    private final String pattern;
-    private String version = null;
-
-    public VersionExtractor(String pattern) {
-        super(new ByteArrayOutputStream());
-
-        this.pattern = pattern;
-    }
-
-    public String getVersion() {
-        if (version == null) {
-            String content
-                    = new String(((ByteArrayOutputStream) out).toByteArray());
-            Pattern p = Pattern.compile(pattern);
-            Matcher matcher = p.matcher(content);
-            if (matcher.find()) {
-                version = matcher.group(1);
-            }
-        }
-        return version;
-    }
-
-    public static boolean isLessThan(String version, String compareTo)
-            throws RuntimeException {
-        if (version == null || version.isEmpty()) {
-            throw new RuntimeException(MessageFormat.format(
-                    I18N.getString("error.version-compare"),
-                    version, compareTo));
-        }
-
-        if (compareTo == null || compareTo.isEmpty()) {
-            throw new RuntimeException(MessageFormat.format(
-                    I18N.getString("error.version-compare"),
-                    version, compareTo));
-        }
-
-        String [] versionArray = version.trim().split(Pattern.quote("."));
-        String [] compareToArray = compareTo.trim().split(Pattern.quote("."));
-
-        for (int i = 0; i < versionArray.length; i++) {
-            int v1 = Integer.parseInt(versionArray[i]);
-            int v2 = Integer.parseInt(compareToArray[i]);
-            if (v1 < v2) {
-                return true;
-            } else if (v1 > v2) {
-                return false;
-            }
-        }
-
-        return false;
-    }
-}
--- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java	Fri Oct 18 14:14:37 2019 -0400
@@ -85,7 +85,7 @@
 
     @Override
     public boolean supported(boolean platformInstaller) {
-        return WinMsiBundler.isSupported();
+        return msiBundler.supported(platformInstaller);
     }
 
     @Override
@@ -96,7 +96,7 @@
     @Override
     public boolean validate(Map<String, ? super Object> params)
             throws ConfigException {
-        return new WinMsiBundler().validate(params);
+        return msiBundler.validate(params);
     }
 
     public File bundle(Map<String, ? super Object> params, File outdir)
@@ -107,7 +107,7 @@
         File exeImageDir = EXE_IMAGE_DIR.fetchFrom(params);
 
         // Write msi to temporary directory.
-        File msi = new WinMsiBundler().bundle(params, exeImageDir);
+        File msi = msiBundler.bundle(params, exeImageDir);
 
         try {
             return buildEXE(msi, outdir);
@@ -151,5 +151,7 @@
         return I18N.getString(key);
     }
 
+    private final WinMsiBundler msiBundler = new WinMsiBundler();
+
     private static native int embedMSI(String exePath, String msiPath);
 }
--- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java	Fri Oct 18 14:14:37 2019 -0400
@@ -33,10 +33,12 @@
 import java.text.MessageFormat;
 import java.util.*;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import javax.xml.stream.XMLOutputFactory;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamWriter;
 import static jdk.jpackage.internal.OverridableResource.createResource;
+import static jdk.jpackage.internal.StandardBundlerParam.*;
 
 import static jdk.jpackage.internal.WindowsBundlerParam.*;
 
@@ -52,10 +54,8 @@
  * directory. The following WiX source files are generated:
  * <ul>
  * <li>main.wxs. Main source file with the installer description
- * <li>bundle.wxi. Source file with application and Java run-time directory tree
- * description. This source file is included from main.wxs
- * <li>icons.wxi. Source file with the list of icons used by the application.
- * This source file is included from main.wxs
+ * <li>bundle.wxf. Source file with application and Java run-time directory tree
+ * description.
  * </ul>
  * <p>
  * main.wxs file is a copy of main.wxs resource from
@@ -86,15 +86,10 @@
  * files.
  * <li>JpIsSystemWide. Set to "yes" if --win-per-user-install command line
  * option was not specified. Undefined otherwise
- * <li>JpWixVersion36OrNewer. Set to "yes" if WiX Toolkit v3.6 or newer is used.
- * Undefined otherwise
  * </ul>
  */
 public class WinMsiBundler  extends AbstractBundler {
 
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-            "jdk.jpackage.internal.resources.WinResources");
-
     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER =
             new WindowsBundlerParam<>(
             "win.app.bundler",
@@ -102,13 +97,6 @@
             params -> new WinAppBundler(),
             null);
 
-    public static final BundlerParamInfo<Boolean> CAN_USE_WIX36 =
-            new WindowsBundlerParam<>(
-            "win.msi.canUseWix36",
-            Boolean.class,
-            params -> false,
-            (s, p) -> Boolean.valueOf(s));
-
     public static final BundlerParamInfo<File> MSI_IMAGE_DIR =
             new WindowsBundlerParam<>(
             "win.msi.imageDir",
@@ -154,66 +142,6 @@
             params -> UUID.randomUUID(),
             (s, p) -> UUID.fromString(s));
 
-    private static final String TOOL_CANDLE = "candle.exe";
-    private static final String TOOL_LIGHT = "light.exe";
-    // autodetect just v3.7, v3.8, 3.9, 3.10 and 3.11
-    private static final String AUTODETECT_DIRS =
-            ";C:\\Program Files (x86)\\WiX Toolset v3.11\\bin;"
-            + "C:\\Program Files\\WiX Toolset v3.11\\bin;"
-            + "C:\\Program Files (x86)\\WiX Toolset v3.10\\bin;"
-            + "C:\\Program Files\\WiX Toolset v3.10\\bin;"
-            + "C:\\Program Files (x86)\\WiX Toolset v3.9\\bin;"
-            + "C:\\Program Files\\WiX Toolset v3.9\\bin;"
-            + "C:\\Program Files (x86)\\WiX Toolset v3.8\\bin;"
-            + "C:\\Program Files\\WiX Toolset v3.8\\bin;"
-            + "C:\\Program Files (x86)\\WiX Toolset v3.7\\bin;"
-            + "C:\\Program Files\\WiX Toolset v3.7\\bin";
-
-    private static String getCandlePath() {
-        for (String dirString : (System.getenv("PATH")
-                + AUTODETECT_DIRS).split(";")) {
-            File f = new File(dirString.replace("\"", ""), TOOL_CANDLE);
-            if (f.isFile()) {
-                return f.toString();
-            }
-        }
-        return null;
-    }
-
-    private static String getLightPath() {
-        for (String dirString : (System.getenv("PATH")
-                + AUTODETECT_DIRS).split(";")) {
-            File f = new File(dirString.replace("\"", ""), TOOL_LIGHT);
-            if (f.isFile()) {
-                return f.toString();
-            }
-        }
-        return null;
-    }
-
-
-    public static final StandardBundlerParam<Boolean> MENU_HINT =
-        new WindowsBundlerParam<>(
-                Arguments.CLIOptions.WIN_MENU_HINT.getId(),
-                Boolean.class,
-                params -> false,
-                // valueOf(null) is false,
-                // and we actually do want null in some cases
-                (s, p) -> (s == null ||
-                        "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
-        );
-
-    public static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
-        new WindowsBundlerParam<>(
-                Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(),
-                Boolean.class,
-                params -> false,
-                // valueOf(null) is false,
-                // and we actually do want null in some cases
-                (s, p) -> (s == null ||
-                       "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
-        );
-
     @Override
     public String getName() {
         return I18N.getString("msi.bundler.name");
@@ -237,17 +165,10 @@
 
     @Override
     public boolean supported(boolean platformInstaller) {
-        return isSupported();
-    }
-
-    @Override
-    public boolean isDefault() {
-        return false;
-    }
-
-    public static boolean isSupported() {
         try {
-            validateWixTools();
+            if (wixToolset == null) {
+                wixToolset = WixTool.toolset();
+            }
             return true;
         } catch (ConfigException ce) {
             Log.error(ce.getMessage());
@@ -260,71 +181,29 @@
         return false;
     }
 
-    private static String findToolVersion(String toolName) {
-        try {
-            if (toolName == null || "".equals(toolName)) return null;
-
-            ProcessBuilder pb = new ProcessBuilder(
-                    toolName,
-                    "/?");
-            VersionExtractor ve = new VersionExtractor("version (\\d+.\\d+)");
-            // not interested in the output
-            IOUtils.exec(pb, true, ve);
-            String version = ve.getVersion();
-            Log.verbose(MessageFormat.format(
-                    I18N.getString("message.tool-version"),
-                    toolName, version));
-            return version;
-        } catch (Exception e) {
-            Log.verbose(e);
-            return null;
-        }
-    }
-
-    public static void validateWixTools() throws ConfigException{
-        String candleVersion = findToolVersion(getCandlePath());
-        String lightVersion = findToolVersion(getLightPath());
-
-        // WiX 3.0+ is required
-        String minVersion = "3.0";
-        if (candleVersion == null || lightVersion == null) {
-            throw new ConfigException(
-                    I18N.getString("error.no-wix-tools"),
-                    I18N.getString("error.no-wix-tools.advice"));
-        }
-
-        if (VersionExtractor.isLessThan(candleVersion, minVersion)) {
-            throw new ConfigException(
-                    MessageFormat.format(
-                    I18N.getString("message.wrong-tool-version"),
-                    TOOL_CANDLE, candleVersion, minVersion),
-                    I18N.getString("error.no-wix-tools.advice"));
-        }
-        if (VersionExtractor.isLessThan(lightVersion, minVersion)) {
-            throw new ConfigException(
-                    MessageFormat.format(
-                    I18N.getString("message.wrong-tool-version"),
-                    TOOL_LIGHT, lightVersion, minVersion),
-                    I18N.getString("error.no-wix-tools.advice"));
-        }
+    @Override
+    public boolean isDefault() {
+        return false;
     }
 
     @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
+            if (wixToolset == null) {
+                wixToolset = WixTool.toolset();
+            }
 
-            String lightVersion = findToolVersion(getLightPath());
-            if (!VersionExtractor.isLessThan(lightVersion, "3.6")) {
-                Log.verbose(I18N.getString("message.use-wix36-features"));
-                params.put(CAN_USE_WIX36.getID(), Boolean.TRUE);
+            for (var toolInfo: wixToolset.values()) {
+                Log.verbose(MessageFormat.format(I18N.getString(
+                        "message.tool-version"), toolInfo.path.getFileName(),
+                        toolInfo.version));
             }
 
+            wixSourcesBuilder.setWixVersion(wixToolset.get(WixTool.Light).version);
+
+            wixSourcesBuilder.logWixFeatures();
+
             /********* validate bundle parameters *************/
 
             String version = PRODUCT_VERSION.fetchFrom(params);
@@ -415,7 +294,7 @@
         return true;
     }
 
-    private boolean prepareProto(Map<String, ? super Object> params)
+    private void prepareProto(Map<String, ? super Object> params)
                 throws PackagerException, IOException {
         File appImage = StandardBundlerParam.getPredefinedAppImage(params);
         File appDir = null;
@@ -445,28 +324,6 @@
             destFile.setWritable(true);
             ensureByMutationFileIsRTF(destFile);
         }
-
-        // copy file association icons
-        List<Map<String, ? super Object>> fileAssociations =
-                FILE_ASSOCIATIONS.fetchFrom(params);
-        for (Map<String, ? super Object> fa : fileAssociations) {
-            File icon = FA_ICON.fetchFrom(fa);
-            if (icon == null) {
-                continue;
-            }
-
-            File faIconFile = new File(appDir, icon.getName());
-
-            if (icon.exists()) {
-                try {
-                    IOUtils.copyFile(icon, faIconFile);
-                } catch (IOException e) {
-                    Log.verbose(e);
-                }
-            }
-        }
-
-        return appDir != null;
     }
 
     public File bundle(Map<String, ? super Object> params, File outdir)
@@ -474,145 +331,37 @@
 
         IOUtils.writableOutputDir(outdir.toPath());
 
-        // validate we have valid tools before continuing
-        String light = getLightPath();
-        String candle = getCandlePath();
-        if (light == null || !new File(light).isFile() ||
-            candle == null || !new File(candle).isFile()) {
-            Log.verbose(MessageFormat.format(
-                   I18N.getString("message.light-file-string"), light));
-            Log.verbose(MessageFormat.format(
-                   I18N.getString("message.candle-file-string"), candle));
-            throw new PackagerException("error.no-wix-tools");
-        }
+        Path imageDir = MSI_IMAGE_DIR.fetchFrom(params).toPath();
+        try {
+            Files.createDirectories(imageDir);
 
-        Map<String, String> wixVars = null;
+            Path postImageScript = imageDir.resolve(APP_NAME.fetchFrom(params) + "-post-image.wsf");
+            createResource(null, params)
+                    .setCategory(I18N.getString("resource.post-install-script"))
+                    .saveToFile(postImageScript);
 
-        File imageDir = MSI_IMAGE_DIR.fetchFrom(params);
-        try {
-            imageDir.mkdirs();
+            prepareProto(params);
 
-            prepareBasicProjectConfig(params);
-            if (prepareProto(params)) {
-                wixVars = prepareWiXConfig(params);
+            wixSourcesBuilder
+            .initFromParams(WIN_APP_IMAGE.fetchFrom(params).toPath(), params)
+            .createMainFragment(CONFIG_ROOT.fetchFrom(params).toPath().resolve(
+                    "bundle.wxf"));
 
-                File configScriptSrc = getConfig_Script(params);
-                if (configScriptSrc.exists()) {
-                    // we need to be running post script in the image folder
-
-                    // NOTE: Would it be better to generate it to the image
-                    // folder and save only if "verbose" is requested?
+            Map<String, String> wixVars = prepareMainProjectFile(params);
 
-                    // for now we replicate it
-                    File configScript =
-                        new File(imageDir, configScriptSrc.getName());
-                    IOUtils.copyFile(configScriptSrc, configScript);
-                    Log.verbose(MessageFormat.format(
-                            I18N.getString("message.running-wsh-script"),
-                            configScript.getAbsolutePath()));
-                    IOUtils.run("wscript", configScript);
-                }
-                return buildMSI(params, wixVars, outdir);
+            if (Files.exists(postImageScript)) {
+                Log.verbose(MessageFormat.format(I18N.getString(
+                        "message.running-wsh-script"),
+                        postImageScript.toAbsolutePath()));
+                Executor.of("wscript", postImageScript.toString()).executeExpectSuccess();
             }
-            return null;
+            return buildMSI(params, wixVars, outdir);
         } catch (IOException ex) {
             Log.verbose(ex);
             throw new PackagerException(ex);
         }
     }
 
-    // name of post-image script
-    private File getConfig_Script(Map<String, ? super Object> params) {
-        return new File(CONFIG_ROOT.fetchFrom(params),
-                APP_NAME.fetchFrom(params) + "-post-image.wsf");
-    }
-
-    private void prepareBasicProjectConfig(
-        Map<String, ? super Object> params) throws IOException {
-
-        Path scriptPath = getConfig_Script(params).toPath();
-
-        createResource(null, params)
-                .setCategory(I18N.getString("resource.post-install-script"))
-                .saveToFile(scriptPath);
-    }
-
-    private static String relativePath(File basedir, File file) {
-        return file.getAbsolutePath().substring(
-                basedir.getAbsolutePath().length() + 1);
-    }
-
-    private AppImageFile appImageFile = null;
-    private String[] getLaunchers( Map<String, ? super Object> params) {
-        try {
-            ArrayList<String> launchers = new ArrayList<String>();
-            if (appImageFile == null) {
-                appImageFile = AppImageFile.load(
-                        WIN_APP_IMAGE.fetchFrom(params).toPath());
-            }
-            launchers.add(appImageFile.getLauncherName());
-            launchers.addAll(appImageFile.getAddLauncherNames());
-            return launchers.toArray(new String[0]);
-        } catch (IOException ioe) {
-            Log.verbose(ioe.getMessage());
-        }
-        String [] launcherNames = new String [1];
-        launcherNames[0] = APP_NAME.fetchFrom(params);
-        return launcherNames;
-    }
-
-    private void prepareIconsFile(
-            Map<String, ? super Object> params) throws IOException {
-
-        File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
-
-        List<Map<String, ? super Object>> addLaunchers =
-                ADD_LAUNCHERS.fetchFrom(params);
-
-        XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
-        try (Writer w = new BufferedWriter(new FileWriter(new File(
-                CONFIG_ROOT.fetchFrom(params), "icons.wxi")))) {
-            XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w);
-
-            xml.writeStartDocument();
-            xml.writeStartElement("Include");
-
-            String[] launcherNames = getLaunchers(params);
-
-            File[] icons = new File[launcherNames.length];
-            for (int i=0; i<launcherNames.length; i++) {
-                icons[i] = new File(imageRootDir, launcherNames[i] + ".ico");
-            }
-
-            for (int i = 0; i < icons.length; i++) {
-                if (icons[i].exists()) {
-                    String iconPath = icons[i].getAbsolutePath();
-
-                    if (MENU_HINT.fetchFrom(params)) {
-                        xml.writeStartElement("Icon");
-                        xml.writeAttribute("Id", "StartMenuIcon.exe" + i);
-                        xml.writeAttribute("SourceFile", iconPath);
-                        xml.writeEndElement();
-                    }
-                    if (SHORTCUT_HINT.fetchFrom(params)) {
-                        xml.writeStartElement("Icon");
-                        xml.writeAttribute("Id", "DesktopIcon.exe" + i);
-                        xml.writeAttribute("SourceFile", iconPath);
-                        xml.writeEndElement();
-                    }
-                }
-            }
-
-            xml.writeEndElement();
-            xml.writeEndDocument();
-            xml.flush();
-            xml.close();
-        } catch (XMLStreamException ex) {
-            Log.verbose(ex);
-            throw new IOException(ex);
-        }
-    }
-
     Map<String, String> prepareMainProjectFile(
             Map<String, ? super Object> params) throws IOException {
         Map<String, String> data = new HashMap<>();
@@ -636,10 +385,6 @@
             data.put("JpAllowDowngrades", "yes");
         }
 
-        if (CAN_USE_WIX36.fetchFrom(params)) {
-            data.put("JpWixVersion36OrNewer", "yes");
-        }
-
         data.put("JpAppName", APP_NAME.fetchFrom(params));
         data.put("JpAppDescription", DESCRIPTION.fetchFrom(params));
         data.put("JpAppVendor", VENDOR.fetchFrom(params));
@@ -648,8 +393,6 @@
         data.put("JpConfigDir",
                 CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
 
-        File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
-
         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
             data.put("JpIsSystemWide", "yes");
         }
@@ -689,367 +432,30 @@
 
         return data;
     }
-    private int id;
-    private int compId;
-    private final static String LAUNCHER_ID = "LauncherId";
-
-    private void walkFileTree(Map<String, ? super Object> params,
-            File root, PrintStream out, String prefix) {
-        List<File> dirs = new ArrayList<>();
-        List<File> files = new ArrayList<>();
-
-        if (!root.isDirectory()) {
-            throw new RuntimeException(
-                    MessageFormat.format(
-                            I18N.getString("error.cannot-walk-directory"),
-                            root.getAbsolutePath()));
-        }
-
-        // sort to files and dirs
-        File[] children = root.listFiles();
-        if (children != null) {
-            for (File f : children) {
-                if (f.isDirectory()) {
-                    dirs.add(f);
-                } else {
-                    files.add(f);
-                }
-            }
-        }
-
-        // have files => need to output component
-        out.println(prefix + " <Component Id=\"comp" + (compId++)
-                + "\" DiskId=\"1\""
-                + " Guid=\"" + UUID.randomUUID().toString() + "\""
-                + " Win64=\"yes\""
-                + ">");
-        out.println(prefix + "  <CreateFolder/>");
-        out.println(prefix + "  <RemoveFolder Id=\"RemoveDir"
-                + (id++) + "\" On=\"uninstall\" />");
-
-
-        File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
-
-        // Find out if we need to use registry. We need it if
-        //  - we doing user level install as file can not serve as KeyPath
-        //  - if we adding shortcut in this component
-        boolean menuShortcut = MENU_HINT.fetchFrom(params);
-        boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
-        boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params) ||
-                menuShortcut || desktopShortcut;
-
-        if (needRegistryKey) {
-            // has to be under HKCU to make WiX happy
-            out.println(prefix + "    <RegistryKey Root=\"HKCU\" "
-                    + " Key=\"Software\\" + VENDOR.fetchFrom(params) + "\\"
-                    + APP_NAME.fetchFrom(params) + "\""
-                    + (CAN_USE_WIX36.fetchFrom(params) ?
-                    ">" : " Action=\"createAndRemoveOnUninstall\">"));
-            out.println(prefix
-                    + "     <RegistryValue Name=\"Version\" Value=\""
-                    + VERSION.fetchFrom(params)
-                    + "\" Type=\"string\" KeyPath=\"yes\"/>");
-            out.println(prefix + "   </RegistryKey>");
-        }
-
-        String[] launcherNames = getLaunchers(params);
-
-        File[] launcherFiles = new File[launcherNames.length];
-        for (int i=0; i<launcherNames.length; i++) {
-            launcherFiles[i] =
-                    new File(imageRootDir, launcherNames[i] + ".exe");
-        }
-        Map<String, String> idToFileMap = new TreeMap<>();
-        boolean launcherSet = false;
-
-        for (File f : files) {
-            boolean isMainLauncher =
-                    launcherFiles.length > 0 && f.equals(launcherFiles[0]);
-
-            launcherSet = launcherSet || isMainLauncher;
-
-            String thisFileId = isMainLauncher ? LAUNCHER_ID : ("FileId" + (id++));
-            idToFileMap.put(f.getName(), thisFileId);
-
-            out.println(prefix + "   <File Id=\"" +
-                    thisFileId + "\""
-                    + " Name=\"" + f.getName() + "\" "
-                    + " Source=\"" + relativePath(imageRootDir, f) + "\""
-                    + " ProcessorArchitecture=\"x64\"" + ">");
-            if (isMainLauncher && desktopShortcut) {
-                out.println(prefix
-                        + "  <Shortcut Id=\"desktopShortcut\" Directory="
-                        + "\"DesktopFolder\""
-                        + " Name=\"" + launcherNames[0]
-                        + "\" WorkingDirectory=\"INSTALLDIR\""
-                        + " Advertise=\"no\" Icon=\"DesktopIcon.exe0\""
-                        + " IconIndex=\"0\" />");
-            }
-            if (isMainLauncher && menuShortcut) {
-                out.println(prefix
-                        + "     <Shortcut Id=\"ExeShortcut\" Directory="
-                        + "\"ProgramMenuDir\""
-                        + " Name=\"" + launcherNames[0]
-                        + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe0\""
-                        + " IconIndex=\"0\" />");
-            }
-
-            // any additional launchers
-            for (int index = 1; index < launcherNames.length; index++ ) {
-
-                if (f.equals(launcherFiles[index])) {
-                    if (desktopShortcut) {
-                        out.println(prefix
-                                + "  <Shortcut Id=\"desktopShortcut"
-                                + index + "\" Directory=\"DesktopFolder\""
-                                + " Name=\"" + launcherNames[index]
-                                + "\" WorkingDirectory=\"INSTALLDIR\""
-                                + " Advertise=\"no\" Icon=\"DesktopIcon.exe"
-                                + index + "\""
-                                + " IconIndex=\"0\" />");
-                    }
-                    if (menuShortcut) {
-                        out.println(prefix
-                            + "     <Shortcut Id=\"ExeShortcut"
-                            + index + "\" Directory=\"ProgramMenuDir\""
-                            + " Name=\"" + launcherNames[index]
-                            + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe"
-                            + index + "\""
-                            + " IconIndex=\"0\" />");
-                    }
-                }
-            }
-            out.println(prefix + "   </File>");
-        }
-
-        if (launcherSet) {
-            List<Map<String, ? super Object>> fileAssociations =
-                FILE_ASSOCIATIONS.fetchFrom(params);
-            Set<String> defaultedMimes = new TreeSet<>();
-            for (Map<String, ? super Object> fa : fileAssociations) {
-                String description = FA_DESCRIPTION.fetchFrom(fa);
-                List<String> extensions = FA_EXTENSIONS.fetchFrom(fa);
-                List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fa);
-                File icon = FA_ICON.fetchFrom(fa);
-
-                String mime = (mimeTypes == null ||
-                    mimeTypes.isEmpty()) ? null : mimeTypes.get(0);
-
-                String entryName = APP_REGISTRY_NAME.fetchFrom(params) + "File";
-
-                if (extensions == null) {
-                    Log.verbose(I18N.getString(
-                          "message.creating-association-with-null-extension"));
-
-                    out.print(prefix + "   <ProgId Id='" + entryName
-                            + "' Description='" + description + "'");
-                    if (icon != null && icon.exists()) {
-                        out.print(" Icon='" + idToFileMap.get(icon.getName())
-                                + "' IconIndex='0'");
-                    }
-                    out.println(" />");
-                } else {
-                    for (String ext : extensions) {
-
-                        entryName = ext.toUpperCase() + "File";
-
-                        out.print(prefix + "   <ProgId Id='" + entryName
-                                + "' Description='" + description + "'");
-                        if (icon != null && icon.exists()) {
-                            out.print(" Icon='"
-                                    + idToFileMap.get(icon.getName())
-                                    + "' IconIndex='0'");
-                        }
-                        out.println(">");
-
-                        out.print(prefix + "    <Extension Id='"
-                                + ext + "' Advertise='no'");
-                        if (mime == null) {
-                            out.println(">");
-                        } else {
-                            out.println(" ContentType='" + mime + "'>");
-                            if (!defaultedMimes.contains(mime)) {
-                                out.println(prefix
-                                        + "      <MIME ContentType='"
-                                        + mime + "' Default='yes' />");
-                                defaultedMimes.add(mime);
-                            }
-                        }
-                        out.println(prefix
-                                + "      <Verb Id='open' Command='Open' "
-                                + "TargetFile='" + LAUNCHER_ID
-                                + "' Argument='\"%1\"' />");
-                        out.println(prefix + "    </Extension>");
-                        out.println(prefix + "   </ProgId>");
-                    }
-                }
-            }
-        }
-
-        out.println(prefix + " </Component>");
-
-        for (File d : dirs) {
-            out.println(prefix + " <Directory Id=\"dirid" + (id++)
-                    + "\" Name=\"" + d.getName() + "\">");
-            walkFileTree(params, d, out, prefix + " ");
-            out.println(prefix + " </Directory>");
-        }
-    }
-
-    void prepareContentList(Map<String, ? super Object> params)
-            throws FileNotFoundException {
-        File f = new File(
-                CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE);
-
-        try (PrintStream out = new PrintStream(f)) {
-
-            // opening
-            out.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
-            out.println("<Include>");
-
-            out.println(" <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">");
-            if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
-                // install to programfiles
-                out.println("  <Directory Id=\"ProgramFiles64Folder\" "
-                            + "Name=\"PFiles\">");
-            } else {
-                // install to user folder
-                out.println(
-                    "  <Directory Name=\"AppData\" Id=\"LocalAppDataFolder\">");
-            }
-
-            // reset counters
-            compId = 0;
-            id = 0;
-
-            // We should get valid folder or subfolders
-            String installDir = WINDOWS_INSTALL_DIR.fetchFrom(params);
-            String [] installDirs = installDir.split(Pattern.quote("\\"));
-            for (int i = 0; i < (installDirs.length - 1); i++)  {
-                out.println("   <Directory Id=\"SUBDIR" + i + "\" Name=\""
-                    + installDirs[i] + "\">");
-                if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
-                    out.println("   <Component Id=\"comp" + (compId++)
-                        + "\" DiskId=\"1\""
-                        + " Guid=\"" + UUID.randomUUID().toString() + "\""
-                        + " Win64=\"yes\""
-                        + ">");
-                    out.println("<CreateFolder/>");
-                    // has to be under HKCU to make WiX happy
-                    out.println("    <RegistryKey Root=\"HKCU\" "
-                        + " Key=\"Software\\" + VENDOR.fetchFrom(params) + "\\"
-                        + APP_NAME.fetchFrom(params) + "\""
-                        + (CAN_USE_WIX36.fetchFrom(params) ?
-                        ">" : " Action=\"createAndRemoveOnUninstall\">"));
-                    out.println("     <RegistryValue Name=\"Version\" Value=\""
-                        + VERSION.fetchFrom(params)
-                        + "\" Type=\"string\" KeyPath=\"yes\"/>");
-                    out.println("   </RegistryKey>");
-                    out.println("   <RemoveFolder Id=\"RemoveDir"
-                        + (id++) + "\" Directory=\"SUBDIR" + i
-                        + "\" On=\"uninstall\" />");
-                    out.println("</Component>");
-                }
-            }
-
-            out.println("   <Directory Id=\"APPLICATIONFOLDER\" Name=\""
-                    + installDirs[installDirs.length - 1] + "\">");
-
-            // dynamic part
-            walkFileTree(params, WIN_APP_IMAGE.fetchFrom(params), out, "    ");
-
-            // closing
-            for (int i = 0; i < installDirs.length; i++)  {
-                out.println("   </Directory>");
-            }
-            out.println("  </Directory>");
-
-            // for shortcuts
-            if (SHORTCUT_HINT.fetchFrom(params)) {
-                out.println("  <Directory Id=\"DesktopFolder\" />");
-            }
-            if (MENU_HINT.fetchFrom(params)) {
-                out.println("  <Directory Id=\"ProgramMenuFolder\">");
-                out.println("    <Directory Id=\"ProgramMenuDir\" Name=\""
-                        + MENU_GROUP.fetchFrom(params) + "\">");
-                out.println("      <Component Id=\"comp" + (compId++) + "\""
-                        + " Guid=\"" + UUID.randomUUID().toString() + "\""
-                        + " Win64=\"yes\""
-                        + ">");
-                out.println("        <RemoveFolder Id=\"ProgramMenuDir\" "
-                        + "On=\"uninstall\" />");
-                // This has to be under HKCU to make WiX happy.
-                // There are numberous discussions on this amoung WiX users
-                // (if user A installs and user B uninstalls key is left behind)
-                // there are suggested workarounds but none are appealing.
-                // Leave it for now
-                out.println(
-                        "         <RegistryValue Root=\"HKCU\" Key=\"Software\\"
-                        + VENDOR.fetchFrom(params) + "\\"
-                        + APP_NAME.fetchFrom(params)
-                        + "\" Type=\"string\" Value=\"\" />");
-                out.println("      </Component>");
-                out.println("    </Directory>");
-                out.println(" </Directory>");
-            }
-
-            out.println(" </Directory>");
-
-            out.println(" <Feature Id=\"DefaultFeature\" "
-                    + "Title=\"Main Feature\" Level=\"1\">");
-            for (int j = 0; j < compId; j++) {
-                out.println("    <ComponentRef Id=\"comp" + j + "\" />");
-            }
-            // component is defined in the main.wsx
-            out.println(
-                    "    <ComponentRef Id=\"CleanupMainApplicationFolder\" />");
-            out.println(" </Feature>");
-            out.println("</Include>");
-
-        }
-    }
 
     private File getConfig_ProjectFile(Map<String, ? super Object> params) {
-        return new File(CONFIG_ROOT.fetchFrom(params),
-                APP_NAME.fetchFrom(params) + ".wxs");
+        return new File(CONFIG_ROOT.fetchFrom(params), "main.wxs");
     }
 
-    private Map<String, String> prepareWiXConfig(
-            Map<String, ? super Object> params) throws IOException {
-        prepareContentList(params);
-        prepareIconsFile(params);
-        return prepareMainProjectFile(params);
-    }
-
-    private final static String MSI_PROJECT_CONTENT_FILE = "bundle.wxi";
-
     private File buildMSI(Map<String, ? super Object> params,
             Map<String, String> wixVars, File outdir)
             throws IOException {
-        File tmpDir = new File(TEMP_ROOT.fetchFrom(params), "tmp");
-        File candleOut = new File(
-                tmpDir, APP_NAME.fetchFrom(params) + ".wixobj");
+
         File msiOut = new File(
                 outdir, INSTALLER_FILE_NAME.fetchFrom(params) + ".msi");
 
         Log.verbose(MessageFormat.format(I18N.getString(
                 "message.preparing-msi-config"), msiOut.getAbsolutePath()));
 
-        msiOut.getParentFile().mkdirs();
-
-        List<String> commandLine = new ArrayList<>(Arrays.asList(
-                getCandlePath(),
-                "-nologo",
-                getConfig_ProjectFile(params).getAbsolutePath(),
-                "-ext", "WixUtilExtension",
-                "-out", candleOut.getAbsolutePath()));
-        for(Map.Entry<String, String> wixVar: wixVars.entrySet()) {
-            String v = "-d" + wixVar.getKey() + "=" + wixVar.getValue();
-            commandLine.add(v);
-        }
-        ProcessBuilder pb = new ProcessBuilder(commandLine);
-        pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
-        IOUtils.exec(pb);
+        WixPipeline wixPipeline = new WixPipeline()
+        .setToolset(wixToolset.entrySet().stream().collect(
+                Collectors.toMap(
+                        entry -> entry.getKey(),
+                        entry -> entry.getValue().path)))
+        .setWixObjDir(TEMP_ROOT.fetchFrom(params).toPath().resolve("wixobj"))
+        .setWorkDir(WIN_APP_IMAGE.fetchFrom(params).toPath())
+        .addSource(CONFIG_ROOT.fetchFrom(params).toPath().resolve("main.wxs"), wixVars)
+        .addSource(CONFIG_ROOT.fetchFrom(params).toPath().resolve("bundle.wxf"), null);
 
         Log.verbose(MessageFormat.format(I18N.getString(
                 "message.generating-msi"), msiOut.getAbsolutePath()));
@@ -1057,44 +463,25 @@
         boolean enableLicenseUI = (LICENSE_FILE.fetchFrom(params) != null);
         boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params);
 
-        commandLine = new ArrayList<>();
+        List<String> lightArgs = new ArrayList<>();
 
-        commandLine.add(getLightPath());
-
-        commandLine.add("-nologo");
-        commandLine.add("-spdb");
         if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
-            commandLine.add("-sice:ICE91");
+            wixPipeline.addLightOptions("-sice:ICE91");
         }
-        commandLine.add(candleOut.getAbsolutePath());
-        commandLine.add("-ext");
-        commandLine.add("WixUtilExtension");
         if (enableLicenseUI || enableInstalldirUI) {
-            commandLine.add("-ext");
-            commandLine.add("WixUIExtension");
+            wixPipeline.addLightOptions("-ext", "WixUIExtension");
         }
 
-        commandLine.add("-loc");
-        commandLine.add(new File(CONFIG_ROOT.fetchFrom(params), I18N.getString(
-                "resource.wxl-file-name")).getAbsolutePath());
+        wixPipeline.addLightOptions("-loc",
+                CONFIG_ROOT.fetchFrom(params).toPath().resolve(I18N.getString(
+                        "resource.wxl-file-name")).toAbsolutePath().toString());
 
         // Only needed if we using CA dll, so Wix can find it
         if (enableInstalldirUI) {
-            commandLine.add("-b");
-            commandLine.add(CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
+            wixPipeline.addLightOptions("-b", CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
         }
 
-        commandLine.add("-out");
-        commandLine.add(msiOut.getAbsolutePath());
-
-        // create .msi
-        pb = new ProcessBuilder(commandLine);
-
-        pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
-        IOUtils.exec(pb);
-
-        candleOut.delete();
-        IOUtils.deleteRecursive(tmpDir);
+        wixPipeline.buildMsi(msiOut.toPath().toAbsolutePath());
 
         return msiOut;
     }
@@ -1170,4 +557,7 @@
 
     }
 
+    private Map<WixTool, WixTool.ToolInfo> wixToolset;
+    private WixSourcesBuilder wixSourcesBuilder = new WixSourcesBuilder();
+
 }
--- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsBundlerParam.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsBundlerParam.java	Fri Oct 18 14:14:37 2019 -0400
@@ -60,18 +60,6 @@
             },
             (s, p) -> s);
 
-    static final BundlerParamInfo<String> APP_REGISTRY_NAME =
-            new StandardBundlerParam<> (
-            "win.registryName",
-            String.class,
-            params -> {
-                String nm = APP_NAME.fetchFrom(params);
-                if (nm == null) return null;
-
-                return nm.replaceAll("[^-a-zA-Z\\.0-9]", "");
-            },
-            (s, p) -> s);
-
     static final StandardBundlerParam<String> MENU_GROUP =
             new StandardBundlerParam<>(
                     Arguments.CLIOptions.WIN_MENU_GROUP.getId(),
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixPipeline.java	Fri Oct 18 14:14:37 2019 -0400
@@ -0,0 +1,145 @@
+/*
+ * 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.*;
+import java.util.function.UnaryOperator;
+import java.util.stream.Stream;
+
+/**
+ * WiX pipeline. Compiles and links WiX sources.
+ */
+public class WixPipeline {
+    WixPipeline() {
+        sources = new ArrayList<>();
+        lightOptions = new ArrayList<>();
+    }
+
+    WixPipeline setToolset(Map<WixTool, Path> v) {
+        toolset = v;
+        return this;
+    }
+
+    WixPipeline setWixVariables(Map<String, String> v) {
+        wixVariables = v;
+        return this;
+    }
+
+    WixPipeline setWixObjDir(Path v) {
+        wixObjDir = v;
+        return this;
+    }
+
+    WixPipeline setWorkDir(Path v) {
+        workDir = v;
+        return this;
+    }
+
+    WixPipeline addSource(Path source, Map<String, String> wixVariables) {
+        WixSource entry = new WixSource();
+        entry.source = source;
+        entry.variables = wixVariables;
+        sources.add(entry);
+        return this;
+    }
+
+    WixPipeline addLightOptions(String ... v) {
+        lightOptions.addAll(List.of(v));
+        return this;
+    }
+
+    void buildMsi(Path msi) throws IOException {
+        List<Path> wixObjs = new ArrayList<>();
+        for (var source : sources) {
+            wixObjs.add(compile(source));
+        }
+
+        List<String> lightCmdline = new ArrayList<>(List.of(
+                toolset.get(WixTool.Light).toString(),
+                "-nologo",
+                "-spdb",
+                "-ext", "WixUtilExtension",
+                "-out", msi.toString()
+        ));
+
+        lightCmdline.addAll(lightOptions);
+        wixObjs.stream().map(Path::toString).forEach(lightCmdline::add);
+
+        Files.createDirectories(msi.getParent());
+        execute(lightCmdline);
+    }
+
+    private Path compile(WixSource wixSource) throws IOException {
+        UnaryOperator<Path> adjustPath = path -> {
+            return workDir != null ? path.toAbsolutePath() : path;
+        };
+
+        Path wixObj = adjustPath.apply(wixObjDir).resolve(IOUtils.replaceSuffix(
+                wixSource.source.getFileName(), ".wixobj"));
+
+        List<String> cmdline = new ArrayList<>(List.of(
+                toolset.get(WixTool.Candle).toString(),
+                "-nologo",
+                adjustPath.apply(wixSource.source).toString(),
+                "-ext", "WixUtilExtension",
+                "-arch", "x64",
+                "-out", wixObj.toAbsolutePath().toString()
+        ));
+
+        Map<String, String> appliedVaribales = new HashMap<>();
+        Stream.of(wixVariables, wixSource.variables)
+                .filter(Objects::nonNull)
+                .forEachOrdered(appliedVaribales::putAll);
+
+        appliedVaribales.entrySet().stream().map(wixVar -> String.format("-d%s=%s",
+                wixVar.getKey(), wixVar.getValue())).forEachOrdered(
+                cmdline::add);
+
+        execute(cmdline);
+
+        return wixObj;
+    }
+
+    private void execute(List<String> cmdline) throws IOException {
+        Executor.of(new ProcessBuilder(cmdline).directory(
+                workDir != null ? workDir.toFile() : null)).executeExpectSuccess();
+    }
+
+    private final static class WixSource {
+        Path source;
+        Map<String, String> variables;
+    }
+
+    private Map<WixTool, Path> toolset;
+    private Map<String, String> wixVariables;
+    private List<String> lightOptions;
+    private Path wixObjDir;
+    private Path workDir;
+    private List<WixSource> sources;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixSourcesBuilder.java	Fri Oct 18 14:14:37 2019 -0400
@@ -0,0 +1,839 @@
+/*
+ * 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.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import jdk.jpackage.internal.IOUtils.XmlConsumer;
+import static jdk.jpackage.internal.StandardBundlerParam.*;
+import static jdk.jpackage.internal.WinMsiBundler.*;
+import static jdk.jpackage.internal.WindowsBundlerParam.MENU_GROUP;
+import static jdk.jpackage.internal.WindowsBundlerParam.WINDOWS_INSTALL_DIR;
+
+/**
+ * Creates application WiX source files.
+ */
+class WixSourcesBuilder {
+
+    WixSourcesBuilder setWixVersion(DottedVersion v) {
+        wixVersion = v;
+        return this;
+    }
+
+    WixSourcesBuilder initFromParams(Path appImageRoot,
+            Map<String, ? super Object> params) {
+        Supplier<ApplicationLayout> appImageSupplier = () -> {
+            if (StandardBundlerParam.isRuntimeInstaller(params)) {
+                return ApplicationLayout.javaRuntime();
+            } else {
+                return ApplicationLayout.platformAppImage();
+            }
+        };
+
+        systemWide = MSI_SYSTEM_WIDE.fetchFrom(params);
+
+        registryKeyPath = Path.of("Software",
+                VENDOR.fetchFrom(params),
+                APP_NAME.fetchFrom(params),
+                VERSION.fetchFrom(params)).toString();
+
+        installDir = (systemWide ? PROGRAM_FILES : LOCAL_PROGRAM_FILES).resolve(
+                WINDOWS_INSTALL_DIR.fetchFrom(params));
+
+        do {
+            ApplicationLayout layout = appImageSupplier.get();
+            // Don't want AppImageFile.FILENAME in installed application.
+            // Register it with app image at a role without a match in installed
+            // app layout to exclude it from layout transformation.
+            layout.pathGroup().setPath(new Object(), Path.of(AppImageFile.FILENAME));
+
+            // Want absolute paths to source files in generated WiX sources.
+            // This is to handle scenario if sources would be processed from
+            // differnt current directory.
+            appImage = layout.resolveAt(appImageRoot.toAbsolutePath().normalize());
+        } while (false);
+
+        installedAppImage = appImageSupplier.get().resolveAt(INSTALLDIR);
+
+        shortcutFolders = new HashSet<>();
+        if (SHORTCUT_HINT.fetchFrom(params)) {
+            shortcutFolders.add(ShortcutsFolder.Desktop);
+        }
+        if (MENU_HINT.fetchFrom(params)) {
+            shortcutFolders.add(ShortcutsFolder.ProgramMenu);
+        }
+
+        if (StandardBundlerParam.isRuntimeInstaller(params)) {
+            launcherPaths = Collections.emptyList();
+        } else {
+            launcherPaths = AppImageFile.getLauncherNames(appImageRoot, params).stream()
+                    .map(name -> installedAppImage.launchersDirectory().resolve(name))
+                    .map(WixSourcesBuilder::addExeSuffixToPath)
+                    .collect(Collectors.toList());
+        }
+
+        programMenuFolderName = MENU_GROUP.fetchFrom(params);
+
+        initFileAssociations(params);
+
+        return this;
+    }
+
+    void createMainFragment(Path file) throws IOException {
+        removeFolderItems = new HashMap<>();
+        defaultedMimes = new HashSet<>();
+        IOUtils.createXml(file, xml -> {
+            xml.writeStartElement("Wix");
+            xml.writeDefaultNamespace("http://schemas.microsoft.com/wix/2006/wi");
+            xml.writeNamespace("util",
+                    "http://schemas.microsoft.com/wix/UtilExtension");
+
+            xml.writeStartElement("Fragment");
+
+            addFaComponentGroup(xml);
+
+            addShortcutComponentGroup(xml);
+
+            addFilesComponentGroup(xml);
+
+            xml.writeEndElement();  // <Fragment>
+
+            addIconsFragment(xml);
+
+            xml.writeEndElement(); // <Wix>
+        });
+    }
+
+    void logWixFeatures() {
+        if (wixVersion.compareTo("3.6") >= 0) {
+            Log.verbose(I18N.getString("message.use-wix36-features"));
+        }
+    }
+
+    private void normalizeFileAssociation(FileAssociation fa) {
+        fa.launcherPath = addExeSuffixToPath(
+                installedAppImage.launchersDirectory().resolve(fa.launcherPath));
+
+        if (fa.iconPath != null && !fa.iconPath.toFile().exists()) {
+            fa.iconPath = null;
+        }
+
+        // Filter out empty extensions.
+        fa.extensions = fa.extensions.stream().filter(Predicate.not(
+                String::isEmpty)).collect(Collectors.toList());
+    }
+
+    private static Path addExeSuffixToPath(Path path) {
+        return IOUtils.addSuffix(path, ".exe");
+    }
+
+    private Path getInstalledFaIcoPath(FileAssociation fa) {
+        String fname = String.format("fa_%s.ico", String.join("_", fa.extensions));
+        return installedAppImage.destktopIntegrationDirectory().resolve(fname);
+    }
+
+    private void initFileAssociations(Map<String, ? super Object> params) {
+        associations = FileAssociation.fetchFrom(params).stream()
+                .peek(this::normalizeFileAssociation)
+                // Filter out file associations without extensions.
+                .filter(fa -> !fa.extensions.isEmpty())
+                .collect(Collectors.toList());
+
+        associations.stream().filter(fa -> fa.iconPath != null).forEach(fa -> {
+            // Need to add fa icon in the image.
+            Object key = new Object();
+            appImage.pathGroup().setPath(key, fa.iconPath);
+            installedAppImage.pathGroup().setPath(key, getInstalledFaIcoPath(fa));
+        });
+    }
+
+    private static UUID createNameUUID(String str) {
+        return UUID.nameUUIDFromBytes(str.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private static UUID createNameUUID(Path path, String role) {
+        if (path.isAbsolute() || !ROOT_DIRS.contains(path.getName(0))) {
+            throw throwInvalidPathException(path);
+        }
+        // Paths are case insensitive on Windows
+        String keyPath = path.toString().toLowerCase();
+        if (role != null) {
+            keyPath = role + "@" + keyPath;
+        }
+        return createNameUUID(keyPath);
+    }
+
+    /**
+     * Value for Id attribute of various WiX elements.
+     */
+    enum Id {
+        File,
+        Folder("dir"),
+        Shortcut,
+        ProgId,
+        Icon,
+        CreateFolder("mkdir"),
+        RemoveFolder("rm");
+
+        Id() {
+            this.prefix = name().toLowerCase();
+        }
+
+        Id(String prefix) {
+            this.prefix = prefix;
+        }
+
+        String of(Path path) {
+            if (this == Folder && KNOWN_DIRS.contains(path)) {
+                return path.getFileName().toString();
+            }
+
+            String result = of(path, prefix, name());
+
+            if (this == Icon) {
+                // Icon id constructed from UUID value is too long and triggers
+                // CNDL1000 warning, so use Java hash code instead.
+                result = String.format("%s%d", prefix, result.hashCode()).replace(
+                        "-", "_");
+            }
+
+            return result;
+        }
+
+        private static String of(Path path, String prefix, String role) {
+            Objects.requireNonNull(role);
+            Objects.requireNonNull(prefix);
+            return String.format("%s%s", prefix,
+                    createNameUUID(path, role).toString().replace("-", ""));
+        }
+
+        static String of(Path path, String prefix) {
+            return of(path, prefix, prefix);
+        }
+
+        private final String prefix;
+    }
+
+    enum Component {
+        File(cfg().file()),
+        Shortcut(cfg().file().withRegistryKeyPath()),
+        ProgId(cfg().file().withRegistryKeyPath()),
+        CreateFolder(cfg().withRegistryKeyPath()),
+        RemoveFolder(cfg().withRegistryKeyPath());
+
+        Component() {
+            this.cfg = cfg();
+            this.id = Id.valueOf(name());
+        }
+
+        Component(Config cfg) {
+            this.cfg = cfg;
+            this.id = Id.valueOf(name());
+        }
+
+        UUID guidOf(Path path) {
+            return createNameUUID(path, name());
+        }
+
+        String idOf(Path path) {
+            return id.of(path);
+        }
+
+        boolean isRegistryKeyPath() {
+            return cfg.withRegistryKeyPath;
+        }
+
+        boolean isFile() {
+            return cfg.isFile;
+        }
+
+        private static final class Config {
+            Config withRegistryKeyPath() {
+                withRegistryKeyPath = true;
+                return this;
+            }
+
+            Config file() {
+                isFile = true;
+                return this;
+            }
+
+            private boolean isFile;
+            private boolean withRegistryKeyPath;
+        }
+
+        private static Config cfg() {
+            return new Config();
+        }
+
+        private final Config cfg;
+        private final Id id;
+    };
+
+    private static void addComponentGroup(XMLStreamWriter xml, String id,
+            List<String> componentIds) throws XMLStreamException, IOException {
+        xml.writeStartElement("ComponentGroup");
+        xml.writeAttribute("Id", id);
+        componentIds = componentIds.stream().filter(Objects::nonNull).collect(
+                Collectors.toList());
+        for (var componentId : componentIds) {
+            xml.writeStartElement("ComponentRef");
+            xml.writeAttribute("Id", componentId);
+            xml.writeEndElement();
+        }
+        xml.writeEndElement();
+    }
+
+    private String addComponent(XMLStreamWriter xml, Path path,
+            Component role, XmlConsumer xmlConsumer) throws XMLStreamException,
+            IOException {
+
+        final Path directoryRefPath;
+        if (role.isFile()) {
+            directoryRefPath = path.getParent();
+        } else {
+            directoryRefPath = path;
+        }
+
+        xml.writeStartElement("DirectoryRef");
+        xml.writeAttribute("Id", Id.Folder.of(directoryRefPath));
+
+        final String componentId = "c" + role.idOf(path);
+        xml.writeStartElement("Component");
+        xml.writeAttribute("Id", componentId);
+        xml.writeAttribute("Guid", String.format("{%s}", role.guidOf(path)));
+
+        boolean isRegistryKeyPath = !systemWide || role.isRegistryKeyPath();
+        if (isRegistryKeyPath) {
+            addRegistryKeyPath(xml, directoryRefPath);
+            if ((role.isFile() || (role == Component.CreateFolder
+                    && !systemWide)) && !SYSTEM_DIRS.contains(directoryRefPath)) {
+                xml.writeStartElement("RemoveFolder");
+                int counter = Optional.ofNullable(removeFolderItems.get(
+                        directoryRefPath)).orElse(Integer.valueOf(0)).intValue() + 1;
+                removeFolderItems.put(directoryRefPath, counter);
+                xml.writeAttribute("Id", String.format("%s_%d", Id.RemoveFolder.of(
+                        directoryRefPath), counter));
+                xml.writeAttribute("On", "uninstall");
+                xml.writeEndElement();
+            }
+        }
+
+        xml.writeStartElement(role.name());
+        if (role != Component.CreateFolder) {
+            xml.writeAttribute("Id", role.idOf(path));
+        }
+
+        if (!isRegistryKeyPath) {
+            xml.writeAttribute("KeyPath", "yes");
+        }
+
+        xmlConsumer.accept(xml);
+        xml.writeEndElement();
+
+        xml.writeEndElement(); // <Component>
+        xml.writeEndElement(); // <DirectoryRef>
+
+        return componentId;
+    }
+
+    private void addFaComponentGroup(XMLStreamWriter xml)
+            throws XMLStreamException, IOException {
+
+        List<String> componentIds = new ArrayList<>();
+        for (var fa : associations) {
+            componentIds.addAll(addFaComponents(xml, fa));
+        }
+        addComponentGroup(xml, "FileAssociations", componentIds);
+    }
+
+    private void addShortcutComponentGroup(XMLStreamWriter xml) throws
+            XMLStreamException, IOException {
+        List<String> componentIds = new ArrayList<>();
+        Set<ShortcutsFolder> defineShortcutFolders = new HashSet<>();
+        for (var launcherPath : launcherPaths) {
+            for (var folder : shortcutFolders) {
+                String componentId = addShortcutComponent(xml, launcherPath,
+                        folder);
+                if (componentId != null) {
+                    defineShortcutFolders.add(folder);
+                    componentIds.add(componentId);
+                }
+            }
+        }
+
+        for (var folder : defineShortcutFolders) {
+            Path path = folder.getPath(this);
+            componentIds.addAll(addRootBranch(xml, path));
+
+            if (!KNOWN_DIRS.contains(path)) {
+                componentIds.add(addDirectoryCleaner(xml, path));
+            }
+        }
+
+        addComponentGroup(xml, "Shortcuts", componentIds);
+    }
+
+    private String addShortcutComponent(XMLStreamWriter xml, Path launcherPath,
+            ShortcutsFolder folder) throws XMLStreamException, IOException {
+        Objects.requireNonNull(folder);
+
+        if (!INSTALLDIR.equals(launcherPath.getName(0))) {
+            throw throwInvalidPathException(launcherPath);
+        }
+
+        String launcherBasename = IOUtils.replaceSuffix(
+                launcherPath.getFileName(), "").toString();
+
+        Path shortcutPath = folder.getPath(this).resolve(launcherBasename);
+        return addComponent(xml, shortcutPath, Component.Shortcut, unused -> {
+            final Path icoFile = IOUtils.addSuffix(
+                    installedAppImage.destktopIntegrationDirectory().resolve(
+                            launcherBasename), ".ico");
+
+            xml.writeAttribute("Name", launcherBasename);
+            xml.writeAttribute("WorkingDirectory", INSTALLDIR.toString());
+            xml.writeAttribute("Advertise", "no");
+            xml.writeAttribute("IconIndex", "0");
+            xml.writeAttribute("Target", String.format("[#%s]",
+                    Component.File.idOf(launcherPath)));
+            xml.writeAttribute("Icon", Id.Icon.of(icoFile));
+        });
+    }
+
+    private List<String> addFaComponents(XMLStreamWriter xml,
+            FileAssociation fa) throws XMLStreamException, IOException {
+        List<String> components = new ArrayList<>();
+        for (var extension: fa.extensions) {
+            Path path = INSTALLDIR.resolve(String.format("%s_%s", extension,
+                    fa.launcherPath.getFileName()));
+            components.add(addComponent(xml, path, Component.ProgId, unused -> {
+                xml.writeAttribute("Description", fa.description);
+
+                if (fa.iconPath != null) {
+                    xml.writeAttribute("Icon", Id.File.of(getInstalledFaIcoPath(
+                            fa)));
+                    xml.writeAttribute("IconIndex", "0");
+                }
+
+                xml.writeStartElement("Extension");
+                xml.writeAttribute("Id", extension);
+                xml.writeAttribute("Advertise", "no");
+
+                var mimeIt = fa.mimeTypes.iterator();
+                if (mimeIt.hasNext()) {
+                    String mime = mimeIt.next();
+                    xml.writeAttribute("ContentType", mime);
+
+                    if (!defaultedMimes.contains(mime)) {
+                        xml.writeStartElement("MIME");
+                        xml.writeAttribute("ContentType", mime);
+                        xml.writeAttribute("Default", "yes");
+                        xml.writeEndElement();
+                        defaultedMimes.add(mime);
+                    }
+                }
+
+                xml.writeStartElement("Verb");
+                xml.writeAttribute("Id", "open");
+                xml.writeAttribute("Command", "Open");
+                xml.writeAttribute("Argument", "%1");
+                xml.writeAttribute("TargetFile", Id.File.of(fa.launcherPath));
+                xml.writeEndElement(); // <Verb>
+
+                xml.writeEndElement(); // <Extension>
+            }));
+        }
+
+        return components;
+    }
+
+    private List<String> addRootBranch(XMLStreamWriter xml, Path path)
+            throws XMLStreamException, IOException {
+        if (!ROOT_DIRS.contains(path.getName(0))) {
+            throw throwInvalidPathException(path);
+        }
+
+        Function<Path, String> createDirectoryName = dir -> null;
+
+        boolean sysDir = true;
+        int levels = 1;
+        var dirIt = path.iterator();
+        xml.writeStartElement("DirectoryRef");
+        xml.writeAttribute("Id", dirIt.next().toString());
+
+        path = path.getName(0);
+        while (dirIt.hasNext()) {
+            levels++;
+            Path name = dirIt.next();
+            path = path.resolve(name);
+
+            if (sysDir && !SYSTEM_DIRS.contains(path)) {
+                sysDir = false;
+                createDirectoryName = dir -> dir.getFileName().toString();
+            }
+
+            final String directoryId;
+            if (!sysDir && path.equals(installDir)) {
+                directoryId = INSTALLDIR.toString();
+            } else {
+                directoryId = Id.Folder.of(path);
+            }
+            xml.writeStartElement("Directory");
+            xml.writeAttribute("Id", directoryId);
+
+            String directoryName = createDirectoryName.apply(path);
+            if (directoryName != null) {
+                xml.writeAttribute("Name", directoryName);
+            }
+        }
+
+        while (0 != levels--) {
+            xml.writeEndElement();
+        }
+
+        List<String> componentIds = new ArrayList<>();
+        while (!SYSTEM_DIRS.contains(path = path.getParent())) {
+            componentIds.add(addRemoveDirectoryComponent(xml, path));
+        }
+
+        return componentIds;
+    }
+
+    private String addRemoveDirectoryComponent(XMLStreamWriter xml, Path path)
+            throws XMLStreamException, IOException {
+        return addComponent(xml, path, Component.RemoveFolder,
+                unused -> xml.writeAttribute("On", "uninstall"));
+    }
+
+    private List<String> addDirectoryHierarchy(XMLStreamWriter xml)
+            throws XMLStreamException, IOException {
+
+        Set<Path> allDirs = new HashSet<>();
+        Set<Path> emptyDirs = new HashSet<>();
+        appImage.transform(installedAppImage, new PathGroup.TransformHandler() {
+            @Override
+            public void copyFile(Path src, Path dst) throws IOException {
+                Path dir = dst.getParent();
+                createDirectory(dir);
+                emptyDirs.remove(dir);
+            }
+
+            @Override
+            public void createDirectory(final Path dir) throws IOException {
+                if (!allDirs.contains(dir)) {
+                    emptyDirs.add(dir);
+                }
+
+                Path it = dir;
+                while (it != null && allDirs.add(it)) {
+                    it = it.getParent();
+                }
+
+                it = dir;
+                while ((it = it.getParent()) != null && emptyDirs.remove(it));
+            }
+        });
+
+        List<String> componentIds = new ArrayList<>();
+        for (var dir : emptyDirs) {
+            componentIds.add(addComponent(xml, dir, Component.CreateFolder,
+                    unused -> {}));
+        }
+
+        if (!systemWide) {
+            // Per-user install requires <RemoveFolder> component in every
+            // directory.
+            for (var dir : allDirs.stream()
+                    .filter(Predicate.not(emptyDirs::contains))
+                    .filter(Predicate.not(removeFolderItems::containsKey))
+                    .collect(Collectors.toList())) {
+                componentIds.add(addRemoveDirectoryComponent(xml, dir));
+            }
+        }
+
+        allDirs.remove(INSTALLDIR);
+        for (var dir : allDirs) {
+            xml.writeStartElement("DirectoryRef");
+            xml.writeAttribute("Id", Id.Folder.of(dir.getParent()));
+            xml.writeStartElement("Directory");
+            xml.writeAttribute("Id", Id.Folder.of(dir));
+            xml.writeAttribute("Name", dir.getFileName().toString());
+            xml.writeEndElement();
+            xml.writeEndElement();
+        }
+
+        componentIds.addAll(addRootBranch(xml, installDir));
+
+        return componentIds;
+    }
+
+    private void addFilesComponentGroup(XMLStreamWriter xml)
+            throws XMLStreamException, IOException {
+
+        List<Map.Entry<Path, Path>> files = new ArrayList<>();
+        appImage.transform(installedAppImage, new PathGroup.TransformHandler() {
+            @Override
+            public void copyFile(Path src, Path dst) throws IOException {
+                files.add(Map.entry(src, dst));
+            }
+
+            @Override
+            public void createDirectory(final Path dir) throws IOException {
+            }
+        });
+
+        List<String> componentIds = new ArrayList<>();
+        for (var file : files) {
+            Path src = file.getKey();
+            Path dst = file.getValue();
+
+            componentIds.add(addComponent(xml, dst, Component.File, unused -> {
+                xml.writeAttribute("Source", src.normalize().toString());
+                Path name = dst.getFileName();
+                if (!name.equals(src.getFileName())) {
+                    xml.writeAttribute("Name", name.toString());
+                }
+            }));
+        }
+
+        componentIds.addAll(addDirectoryHierarchy(xml));
+
+        componentIds.add(addDirectoryCleaner(xml, INSTALLDIR));
+
+        addComponentGroup(xml, "Files", componentIds);
+    }
+
+    private void addIconsFragment(XMLStreamWriter xml) throws
+            XMLStreamException, IOException {
+
+        PathGroup srcPathGroup = appImage.pathGroup();
+        PathGroup dstPathGroup = installedAppImage.pathGroup();
+
+        // Build list of copy operations for all .ico files in application image
+        List<Map.Entry<Path, Path>> icoFiles = new ArrayList<>();
+        srcPathGroup.transform(dstPathGroup, new PathGroup.TransformHandler() {
+            @Override
+            public void copyFile(Path src, Path dst) throws IOException {
+                if (src.getFileName().toString().endsWith(".ico")) {
+                    icoFiles.add(Map.entry(src, dst));
+                }
+            }
+
+            @Override
+            public void createDirectory(Path dst) throws IOException {
+            }
+        });
+
+        xml.writeStartElement("Fragment");
+        for (var icoFile : icoFiles) {
+            xml.writeStartElement("Icon");
+            xml.writeAttribute("Id", Id.Icon.of(icoFile.getValue()));
+            xml.writeAttribute("SourceFile", icoFile.getKey().toString());
+            xml.writeEndElement();
+        }
+        xml.writeEndElement();
+    }
+
+    private void addRegistryKeyPath(XMLStreamWriter xml, Path path) throws
+            XMLStreamException, IOException {
+        addRegistryKeyPath(xml, path, () -> "ProductCode", () -> "[ProductCode]");
+    }
+
+    private void addRegistryKeyPath(XMLStreamWriter xml, Path path,
+            Supplier<String> nameAttr, Supplier<String> valueAttr) throws
+            XMLStreamException, IOException {
+
+        String regRoot = USER_PROFILE_DIRS.stream().anyMatch(path::startsWith)
+                || !systemWide ? "HKCU" : "HKLM";
+
+        xml.writeStartElement("RegistryKey");
+        xml.writeAttribute("Root", regRoot);
+        xml.writeAttribute("Key", registryKeyPath);
+        if (wixVersion.compareTo("3.6") < 0) {
+            xml.writeAttribute("Action", "createAndRemoveOnUninstall");
+        }
+        xml.writeStartElement("RegistryValue");
+        xml.writeAttribute("Type", "string");
+        xml.writeAttribute("KeyPath", "yes");
+        xml.writeAttribute("Name", nameAttr.get());
+        xml.writeAttribute("Value", valueAttr.get());
+        xml.writeEndElement(); // <RegistryValue>
+        xml.writeEndElement(); // <RegistryKey>
+    }
+
+    private String addDirectoryCleaner(XMLStreamWriter xml, Path path) throws
+            XMLStreamException, IOException {
+        if (wixVersion.compareTo("3.6") < 0) {
+            return null;
+        }
+
+        // rm -rf
+        final String baseId = Id.of(path, "rm_rf");
+        final String propertyId = baseId.toUpperCase();
+        final String componentId = ("c" + baseId);
+
+        xml.writeStartElement("Property");
+        xml.writeAttribute("Id", propertyId);
+        xml.writeStartElement("RegistrySearch");
+        xml.writeAttribute("Id", Id.of(path, "regsearch"));
+        xml.writeAttribute("Root", systemWide ? "HKLM" : "HKCU");
+        xml.writeAttribute("Key", registryKeyPath);
+        xml.writeAttribute("Type", "raw");
+        xml.writeAttribute("Name", propertyId);
+        xml.writeEndElement(); // <RegistrySearch>
+        xml.writeEndElement(); // <Property>
+
+        xml.writeStartElement("DirectoryRef");
+        xml.writeAttribute("Id", INSTALLDIR.toString());
+        xml.writeStartElement("Component");
+        xml.writeAttribute("Id", componentId);
+        xml.writeAttribute("Guid", "*");
+
+        addRegistryKeyPath(xml, INSTALLDIR, () -> propertyId, () -> {
+            // The following code converts a path to value to be saved in registry.
+            // E.g.:
+            //  INSTALLDIR -> [INSTALLDIR]
+            //  TERGETDIR/ProgramFiles64Folder/foo/bar -> [ProgramFiles64Folder]foo/bar
+            final Path rootDir = KNOWN_DIRS.stream()
+                    .sorted(Comparator.comparing(Path::getNameCount).reversed())
+                    .filter(path::startsWith)
+                    .findFirst().get();
+            StringBuilder sb = new StringBuilder();
+            sb.append(String.format("[%s]", rootDir.getFileName().toString()));
+            sb.append(rootDir.relativize(path).toString());
+            return sb.toString();
+        });
+
+        xml.writeStartElement(
+                "http://schemas.microsoft.com/wix/UtilExtension",
+                "RemoveFolderEx");
+        xml.writeAttribute("On", "uninstall");
+        xml.writeAttribute("Property", propertyId);
+        xml.writeEndElement(); // <RemoveFolderEx>
+        xml.writeEndElement(); // <Component>
+        xml.writeEndElement(); // <DirectoryRef>
+
+        return componentId;
+    }
+
+    private static IllegalArgumentException throwInvalidPathException(Path v) {
+        throw new IllegalArgumentException(String.format("Invalid path [%s]", v));
+    }
+
+    enum ShortcutsFolder {
+        ProgramMenu(PROGRAM_MENU_PATH),
+        Desktop(DESKTOP_PATH);
+
+        private ShortcutsFolder(Path root) {
+            this.root = root;
+        }
+
+        Path getPath(WixSourcesBuilder outer) {
+            if (this == ProgramMenu) {
+                return root.resolve(outer.programMenuFolderName);
+            }
+            return root;
+        }
+
+        private final Path root;
+    }
+
+    private DottedVersion wixVersion;
+
+    private boolean systemWide;
+
+    private String registryKeyPath;
+
+    private Path installDir;
+
+    private String programMenuFolderName;
+
+    private List<FileAssociation> associations;
+
+    private Set<ShortcutsFolder> shortcutFolders;
+
+    private List<Path> launcherPaths;
+
+    private ApplicationLayout appImage;
+    private ApplicationLayout installedAppImage;
+
+    private Map<Path, Integer> removeFolderItems;
+    private Set<String> defaultedMimes;
+
+    private final static Path TARGETDIR = Path.of("TARGETDIR");
+
+    private final static Path INSTALLDIR = Path.of("INSTALLDIR");
+
+    private final static Set<Path> ROOT_DIRS = Set.of(INSTALLDIR, TARGETDIR);
+
+    private final static Path PROGRAM_MENU_PATH = TARGETDIR.resolve("ProgramMenuFolder");
+
+    private final static Path DESKTOP_PATH = TARGETDIR.resolve("DesktopFolder");
+
+    private final static Path PROGRAM_FILES = TARGETDIR.resolve("ProgramFiles64Folder");
+
+    private final static Path LOCAL_PROGRAM_FILES = TARGETDIR.resolve("LocalAppDataFolder");
+
+    private final static Set<Path> SYSTEM_DIRS = Set.of(TARGETDIR,
+            PROGRAM_MENU_PATH, DESKTOP_PATH, PROGRAM_FILES, LOCAL_PROGRAM_FILES);
+
+    private final static Set<Path> KNOWN_DIRS = Stream.of(Set.of(INSTALLDIR),
+            SYSTEM_DIRS).flatMap(Set::stream).collect(
+            Collectors.toUnmodifiableSet());
+
+    private final static Set<Path> USER_PROFILE_DIRS = Set.of(LOCAL_PROGRAM_FILES,
+            PROGRAM_MENU_PATH, DESKTOP_PATH);
+
+    private static final StandardBundlerParam<Boolean> MENU_HINT =
+        new WindowsBundlerParam<>(
+                Arguments.CLIOptions.WIN_MENU_HINT.getId(),
+                Boolean.class,
+                params -> false,
+                // valueOf(null) is false,
+                // and we actually do want null in some cases
+                (s, p) -> (s == null ||
+                        "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
+        );
+
+    private static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
+        new WindowsBundlerParam<>(
+                Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(),
+                Boolean.class,
+                params -> false,
+                // valueOf(null) is false,
+                // and we actually do want null in some cases
+                (s, p) -> (s == null ||
+                       "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
+        );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java	Fri Oct 18 14:14:37 2019 -0400
@@ -0,0 +1,146 @@
+/*
+ * 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.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * WiX tool.
+ */
+public enum WixTool {
+    Candle, Light;
+
+    static final class ToolInfo {
+        ToolInfo(Path path, String version) {
+            this.path = path;
+            this.version = new DottedVersion(version);
+        }
+
+        final Path path;
+        final DottedVersion version;
+    }
+
+    static Map<WixTool, ToolInfo> toolset() throws ConfigException {
+        Map<WixTool, ToolInfo> toolset = new HashMap<>();
+        for (var tool : values()) {
+            toolset.put(tool, tool.find());
+        }
+        return toolset;
+    }
+
+    ToolInfo find() throws ConfigException {
+        final Path toolFileName = IOUtils.addSuffix(
+                Path.of(name().toLowerCase()), ".exe");
+
+        String[] version = new String[1];
+        ConfigException reason = createToolValidator(toolFileName, version).get();
+        if (version[0] != null) {
+            if (reason == null) {
+                // Found in PATH.
+                return new ToolInfo(toolFileName, version[0]);
+            }
+
+            // Found in PATH, but something went wrong.
+            throw reason;
+        }
+
+        for (var dir : findWixInstallDirs()) {
+            Path path = dir.resolve(toolFileName);
+            if (path.toFile().exists()) {
+                reason = createToolValidator(path, version).get();
+                if (reason != null) {
+                    throw reason;
+                }
+                return new ToolInfo(path, version[0]);
+            }
+        }
+
+        throw reason;
+    }
+
+    private static Supplier<ConfigException> createToolValidator(Path toolPath,
+            String[] versionCtnr) {
+        return new ToolValidator(toolPath)
+                .setCommandLine("/?")
+                .setMinimalVersion(MINIMAL_VERSION)
+                .setToolNotFoundErrorHandler(
+                        (name, ex) -> new ConfigException(
+                                I18N.getString("error.no-wix-tools"),
+                                I18N.getString("error.no-wix-tools.advice")))
+                .setToolOldVersionErrorHandler(
+                        (name, version) -> new ConfigException(
+                                MessageFormat.format(I18N.getString(
+                                        "message.wrong-tool-version"), name,
+                                        version, MINIMAL_VERSION),
+                                I18N.getString("error.no-wix-tools.advice")))
+                .setVersionParser(output -> {
+                    versionCtnr[0] = "";
+                    String firstLineOfOutput = output.findFirst().orElse("");
+                    int separatorIdx = firstLineOfOutput.lastIndexOf(' ');
+                    if (separatorIdx == -1) {
+                        return null;
+                    }
+                    versionCtnr[0] = firstLineOfOutput.substring(separatorIdx + 1);
+                    return versionCtnr[0];
+                })::validate;
+    }
+
+    private final static String MINIMAL_VERSION = "3.0";
+
+    private final static Path PROGRAM_FILES = Path.of("C:\\Program Files");
+    private final static Path PROGRAM_FILES_X86 = Path.of("C:\\Program Files (x86)");
+
+    private static List<Path> findWixInstallDirs() {
+        PathMatcher wixInstallDirMatcher = FileSystems.getDefault().getPathMatcher(
+                "glob:WiX Toolset v*");
+
+        // Returns list of WiX install directories ordered by WiX version number.
+        // Newer versions go first.
+        return Stream.of(PROGRAM_FILES, PROGRAM_FILES_X86).map(path -> {
+            List<Path> result;
+            try (var paths = Files.walk(path, 1)) {
+                result = paths.collect(Collectors.toList());
+            } catch (IOException ex) {
+                Log.verbose(ex);
+                result = Collections.emptyList();
+            }
+            return result;
+        }).flatMap(List::stream)
+        .filter(path -> wixInstallDirMatcher.matches(path.getFileName()))
+        .sorted(Comparator.comparing(Path::getFileName).reversed())
+        .map(path -> path.resolve("bin"))
+        .collect(Collectors.toList());
+    }
+}
--- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs	Fri Oct 18 11:00:57 2019 -0400
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs	Fri Oct 18 14:14:37 2019 -0400
@@ -1,15 +1,13 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
      xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
-     
+
   <?ifdef JpIsSystemWide ?>
     <?define JpInstallScope="perMachine"?>
-    <?define JpRegistryRoot="HKLM"?>
   <?else?>
     <?define JpInstallScope="perUser"?>
-    <?define JpRegistryRoot="HKCU"?>
   <?endif?>
-  
+
   <Product  Id="$(var.JpProductCode)" Name="$(var.JpAppName)"
             Language="1033" Version="$(var.JpAppVersion)"
             Manufacturer="$(var.JpAppVendor)"
@@ -23,90 +21,67 @@
     <?ifdef JpAllowDowngrades ?>
     <MajorUpgrade AllowDowngrades="yes"/>
     <?endif?>
-    
-        <!-- We use RemoveFolderEx to ensure application folder is fully
-             removed on uninstall. Including files created outside of MSI
-             after application had been installed (e.g. on AU or user state).
+
+    <!-- Standard required root -->
+    <Directory Id="TARGETDIR" Name="SourceDir"/>
+
+    <Feature Id="DefaultFeature" Title="Main Feature" Level="1">
+      <ComponentGroupRef Id="Shortcuts"/>
+      <ComponentGroupRef Id="Files"/>
+      <ComponentGroupRef Id="FileAssociations"/>
+    </Feature>
 
-             However, RemoveFolderEx is only available in WiX 3.6,
-             we will comment it out if we running older WiX.
+    <?ifdef JpInstallDirChooser ?>
+    <Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
+    <CustomAction Id="JpCheckInstallDir" BinaryKey="JpCaDll" DllEntry="CheckInstallDir" />
+    <?endif?>
+
+    <UI>
+      <?ifdef JpInstallDirChooser ?>
+      <Dialog Id="JpInvalidInstallDir" Width="300" Height="85" Title="[ProductName] Setup" NoMinimize="yes">
+        <Control Id="JpInvalidInstallDirYes" Type="PushButton" X="100" Y="55" Width="50" Height="15" Default="no" Cancel="no" Text="Yes">
+          <Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
+        </Control>
+        <Control Id="JpInvalidInstallDirNo" Type="PushButton" X="150" Y="55" Width="50" Height="15" Default="yes" Cancel="yes" Text="No">
+          <Publish Event="NewDialog" Value="InstallDirDlg">1</Publish>
+        </Control>
+        <Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no">
+          <Text>!(loc.message.install.dir.exist)</Text>
+        </Control>
+      </Dialog>
 
-             RemoveFolderEx requires that we "remember" the path for uninstall.
-             Read the path value and set the APPLICATIONFOLDER property with the value.
-        -->
-        <Property Id="APPLICATIONFOLDER">
-            <RegistrySearch Key="SOFTWARE\$(var.JpAppVendor)\$(var.JpAppName)"
-                            Root="$(var.JpRegistryRoot)" Type="raw" Win64="yes"
-                            Id="APPLICATIONFOLDER_REGSEARCH" Name="Path" />
-        </Property>
-        <DirectoryRef Id="APPLICATIONFOLDER">
-            <Component Id="CleanupMainApplicationFolder" Guid="*" Win64="yes">
-                <RegistryValue Root="$(var.JpRegistryRoot)"
-                               Key="SOFTWARE\$(var.JpAppVendor)\$(var.JpAppName)"
-                               Name="Path" Type="string" Value="[APPLICATIONFOLDER]"
-                               KeyPath="yes" />
-                <!-- We need to use APPLICATIONFOLDER variable here or RemoveFolderEx
-                     will not remove on "install". But only if WiX 3.6 is used. -->
-                  <?ifdef JpWixVersion36OrNewer ?>
-                  <util:RemoveFolderEx On="uninstall" Property="APPLICATIONFOLDER" />
-                  <?endif?>
-            </Component>
-        </DirectoryRef>
-        
-        <?include $(var.JpConfigDir)/bundle.wxi ?>
-        
-        <?ifdef JpInstallDirChooser ?>
-        <Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
-        <CustomAction Id="JpCheckInstallDir" BinaryKey="JpCaDll" DllEntry="CheckInstallDir" />
-        <?endif?>
-        
-        <UI>
-          <?ifdef JpInstallDirChooser ?>
-          <Dialog Id="JpInvalidInstallDir" Width="300" Height="85" Title="[ProductName] Setup" NoMinimize="yes">
-            <Control Id="JpInvalidInstallDirYes" Type="PushButton" X="100" Y="55" Width="50" Height="15" Default="no" Cancel="no" Text="Yes">
-              <Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
-            </Control>
-            <Control Id="JpInvalidInstallDirNo" Type="PushButton" X="150" Y="55" Width="50" Height="15" Default="yes" Cancel="yes" Text="No">
-              <Publish Event="NewDialog" Value="InstallDirDlg">1</Publish>
-            </Control>
-            <Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no">
-              <Text>!(loc.message.install.dir.exist)</Text>
-            </Control>
-          </Dialog>
-          
-          <!-- 
-            Run WixUI_InstallDir dialog in the default install directory.
-          -->
-          <Property Id="WIXUI_INSTALLDIR" Value="APPLICATIONFOLDER"/>
-          <UIRef Id="WixUI_InstallDir" />
+      <!--
+        Run WixUI_InstallDir dialog in the default install directory.
+      -->
+      <Property Id="WIXUI_INSTALLDIR" Value="APPLICATIONFOLDER"/>
+      <UIRef Id="WixUI_InstallDir" />
+
+      <Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="JpCheckInstallDir" Order="3">1</Publish>
+      <Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="JpInvalidInstallDir" Order="5">INSTALLDIR_VALID="0"</Publish>
+      <Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="5">INSTALLDIR_VALID="1"</Publish>
 
-          <Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="JpCheckInstallDir" Order="3">1</Publish>
-          <Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="JpInvalidInstallDir" Order="5">INSTALLDIR_VALID="0"</Publish>
-          <Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="5">INSTALLDIR_VALID="1"</Publish>
-          
-          <?ifndef JpLicenseRtf ?>
-          <!-- 
-            No license file provided. 
-            Override the dialog sequence in built-in dialog set "WixUI_InstallDir"
-            to exclude license dialog. 
-          -->
-          <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">1</Publish>
-          <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">1</Publish>
-          <?endif?>
+      <?ifndef JpLicenseRtf ?>
+      <!--
+        No license file provided.
+        Override the dialog sequence in built-in dialog set "WixUI_InstallDir"
+        to exclude license dialog.
+      -->
+      <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">1</Publish>
+      <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">1</Publish>
+      <?endif?>
+
+      <?else?>
 
-          <?else?>
-          
-          <?ifdef JpLicenseRtf ?>
-          <UIRef Id="WixUI_Minimal" />
-          <?endif?>
-          
-          <?endif?>
-        </UI>
-        
-        <?ifdef JpLicenseRtf ?>
-        <WixVariable Id="WixUILicenseRtf" Value="$(var.JpLicenseRtf)"/>
-        <?endif?>
-        
-        <?include $(var.JpConfigDir)/icons.wxi ?>
-    </Product>
+      <?ifdef JpLicenseRtf ?>
+      <UIRef Id="WixUI_Minimal" />
+      <?endif?>
+
+      <?endif?>
+    </UI>
+
+    <?ifdef JpLicenseRtf ?>
+    <WixVariable Id="WixUILicenseRtf" Value="$(var.JpLicenseRtf)"/>
+    <?endif?>
+
+  </Product>
 </Wix>
Binary file test/jdk/tools/jpackage/helpers/jdk/jpackage/test/.Executor.java.swp has changed
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/FileAssociations.java	Fri Oct 18 14:14:37 2019 -0400
@@ -23,10 +23,11 @@
 package jdk.jpackage.test;
 
 import java.nio.file.Path;
+import java.util.HashMap;
 import java.util.Map;
 
 
-public class FileAssociations {
+final public class FileAssociations {
     public FileAssociations(String faSuffixName) {
         suffixName = faSuffixName;
         setFilename("fa");
@@ -34,22 +35,36 @@
     }
 
     public void createFile() {
-        TKit.createPropertiesFile(file,
-                Map.entry("extension", suffixName),
-                Map.entry("mime-type", getMime()),
-                Map.entry("description", description));
+        Map<String, String> entries = new HashMap<>(Map.of(
+            "extension", suffixName,
+            "mime-type", getMime(),
+            "description", description
+        ));
+        if (icon != null) {
+            if (TKit.isWindows()) {
+                entries.put("icon", icon.toString().replace("\\", "/"));
+            } else {
+                entries.put("icon", icon.toString());
+            }
+        }
+        TKit.createPropertiesFile(file, entries);
     }
 
-    final public FileAssociations setFilename(String v) {
+    public FileAssociations setFilename(String v) {
         file = TKit.workDir().resolve(v + ".properties");
         return this;
     }
 
-    final public FileAssociations setDescription(String v) {
+    public FileAssociations setDescription(String v) {
         description = v;
         return this;
     }
 
+    public FileAssociations setIcon(Path v) {
+        icon = v;
+        return this;
+    }
+
     public Path getPropertiesFile() {
         return file;
     }
@@ -65,4 +80,5 @@
     private Path file;
     final private String suffixName;
     private String description;
+    private Path icon;
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java	Fri Oct 18 14:14:37 2019 -0400
@@ -126,7 +126,7 @@
         if (moduleName == null && CLASS_NAME.equals(qualifiedClassName)) {
             // Use Hello.java as is.
             cmd.addAction((self) -> {
-                File jarFile = self.inputDir().resolve(jarFileName).toFile();
+                Path jarFile = self.inputDir().resolve(jarFileName);
                 createJarBuilder().setOutputJar(jarFile).addSourceFile(
                         HELLO_JAVA).create();
             });
@@ -144,8 +144,7 @@
                 }
 
                 TKit.withTempDirectory("src",
-                        workDir -> prepareSources(workDir).setOutputJar(
-                                jarFile.toFile()).create());
+                        workDir -> prepareSources(workDir).setOutputJar(jarFile).create());
             });
         }
 
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java	Fri Oct 18 14:14:37 2019 -0400
@@ -40,7 +40,7 @@
         sourceFiles = new ArrayList<>();
     }
 
-    public JarBuilder setOutputJar(File v) {
+    public JarBuilder setOutputJar(Path v) {
         outputJar = v;
         return this;
     }
@@ -69,9 +69,16 @@
                         .addPathArguments(sourceFiles)
                         .execute().assertExitCodeIsZero();
             }
-            Path tmpJar = workDir.resolve("foo.jar");
-            Executor jarExe = new Executor();
-            jarExe.setToolProvider(JavaTool.JAR).addArguments("-c", "-f", tmpJar.toString());
+
+            Files.createDirectories(outputJar.getParent());
+            if (Files.exists(outputJar)) {
+                TKit.trace(String.format("Delete [%s] existing jar file", outputJar));
+                Files.deleteIfExists(outputJar);
+            }
+
+            Executor jarExe = new Executor()
+                    .setToolProvider(JavaTool.JAR)
+                    .addArguments("-c", "-f", outputJar.toString());
             if (moduleVersion != null) {
                 jarExe.addArguments(String.format("--module-version=%s",
                         moduleVersion));
@@ -81,12 +88,10 @@
             }
             jarExe.addArguments("-C", workDir.toString(), ".");
             jarExe.execute().assertExitCodeIsZero();
-            outputJar.getParentFile().mkdirs();
-            Files.copy(tmpJar, outputJar.toPath(), REPLACE_EXISTING);
         });
     }
     private List<Path> sourceFiles;
-    private File outputJar;
+    private Path outputJar;
     private String mainClass;
     private String moduleVersion;
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaAppDesc.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JavaAppDesc.java	Fri Oct 18 14:14:37 2019 -0400
@@ -22,7 +22,8 @@
  */
 package jdk.jpackage.test;
 
-import java.util.Objects;
+import java.io.File;
+import java.nio.file.Path;
 
 
 public final class JavaAppDesc {
@@ -58,6 +59,11 @@
         return qualifiedClassName;
     }
 
+    public Path classFilePath() {
+        return Path.of(qualifiedClassName.replace(".", File.separator)
+                + ".class");
+    }
+
     public String moduleName() {
         return moduleName;
     }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java	Fri Oct 18 14:14:37 2019 -0400
@@ -33,6 +33,7 @@
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import jdk.jpackage.test.Functional.ThrowingConsumer;
+import jdk.jpackage.internal.AppImageFile;
 import static jdk.jpackage.test.PackageType.*;
 
 /**
@@ -83,7 +84,8 @@
         return this;
     }
 
-    private PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v, String id) {
+    private PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v,
+            String id) {
         if (id != null) {
             if (namedInitializers.contains(id)) {
                 return this;
@@ -149,8 +151,10 @@
         return this;
     }
 
-    static void withTestFileAssociationsFile(FileAssociations fa, ThrowingConsumer<Path> consumer) {
-        final String testFileDefaultName = String.join(".", "test", fa.getSuffix());
+    static void withTestFileAssociationsFile(FileAssociations fa,
+            ThrowingConsumer<Path> consumer) {
+        final String testFileDefaultName = String.join(".", "test",
+                fa.getSuffix());
         TKit.withTempFile(testFileDefaultName, fa.getSuffix(), testFile -> {
             if (TKit.isLinux()) {
                 LinuxHelper.initFileAssociationsTestFile(testFile);
@@ -347,6 +351,9 @@
                 }
             }
 
+            TKit.assertPathExists(cmd.appInstallationDirectory().resolve(
+                    AppImageFile.FILENAME), false);
+
             installVerifiers.stream().forEach(v -> v.accept(cmd));
         }
 
@@ -413,7 +420,8 @@
             bundleOutputDir = new File(val).getAbsoluteFile();
 
             if (!bundleOutputDir.isDirectory()) {
-                throw new IllegalArgumentException(String.format("Invalid value of %s sytem property: [%s]. Should be existing directory",
+                throw new IllegalArgumentException(String.format(
+                        "Invalid value of %s sytem property: [%s]. Should be existing directory",
                         TKit.getConfigPropertyName(propertyName),
                         bundleOutputDir));
             }
@@ -425,7 +433,9 @@
         String action = Optional.ofNullable(TKit.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]",
+                a -> a.toString().equals(action)).findFirst().orElseThrow(
+                        () -> new IllegalArgumentException(String.format(
+                                "Unrecognized value of %s property: [%s]",
                                 TKit.getConfigPropertyName(propertyName), action)));
     }
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java	Fri Oct 18 14:14:37 2019 -0400
@@ -45,6 +45,8 @@
 
 final public class TKit {
 
+    private static final String OS = System.getProperty("os.name").toLowerCase();
+
     public static final Path TEST_SRC_ROOT = Functional.identity(() -> {
         Path root = Path.of(System.getProperty("test.src"));
 
@@ -58,6 +60,22 @@
         throw new RuntimeException("Failed to locate apps directory");
     }).get();
 
+    public final static String ICON_SUFFIX = Functional.identity(() -> {
+        if (isOSX()) {
+            return ".icns";
+        }
+
+        if (isLinux()) {
+            return ".png";
+        }
+
+        if (isWindows()) {
+            return ".ico";
+        }
+
+        throw throwUnknownPlatformError();
+    }).get();
+
     public static void run(String args[], ThrowingRunnable testBody) {
         if (currentTest != null) {
             throw new IllegalStateException(
@@ -816,6 +834,4 @@
             VERBOSE_TEST_SETUP = isNonOf.test(Set.of("init", "i"));
         }
     }
-
-    private static final String OS = System.getProperty("os.name").toLowerCase();
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TestInstance.java	Fri Oct 18 14:14:37 2019 -0400
@@ -153,6 +153,7 @@
         this.afterActions = Collections.emptyList();
         this.testDesc = TestDesc.createBuilder().get();
         this.dryRun = false;
+        this.workDir = createWorkDirName(testDesc);
     }
 
     TestInstance(MethodCall testBody, List<ThrowingConsumer> beforeActions,
@@ -164,6 +165,7 @@
         this.afterActions = afterActions;
         this.testDesc = testBody.createDescription();
         this.dryRun = dryRun;
+        this.workDir = createWorkDirName(testDesc);
     }
 
     void notifyAssert() {
@@ -205,28 +207,7 @@
     }
 
     Path workDir() {
-        Path result = Path.of(".");
-        List<String> components = new ArrayList<>();
-
-        String testFunctionName = functionName();
-        if (testFunctionName != null) {
-            components.add(testFunctionName);
-        }
-
-        if (isPrametrized()) {
-            components.add(String.format("%08x", fullName().hashCode()));
-        }
-
-        if (!components.isEmpty()) {
-            result = result.resolve(String.join(".", components));
-        }
-
-        return result;
-    }
-
-    boolean isPrametrized() {
-        return Stream.of(testDesc.functionArgs, testDesc.instanceArgs).anyMatch(
-                Objects::nonNull);
+        return workDir;
     }
 
     @Override
@@ -239,7 +220,7 @@
                     testInstance));
             try {
                 if (!dryRun) {
-                    Files.createDirectories(workDir());
+                    Files.createDirectories(workDir);
                     testBody.accept(testInstance);
                 }
             } finally {
@@ -255,7 +236,7 @@
             }
 
             if (!KEEP_WORK_DIR.contains(status)) {
-                TKit.deleteDirectoryRecursive(workDir());
+                TKit.deleteDirectoryRecursive(workDir);
             }
 
             TKit.log(String.format("%s %s; checks=%d", status, fullName,
@@ -274,6 +255,42 @@
         return null;
     }
 
+    private static boolean isCalledByJavatest() {
+        StackTraceElement st[] = Thread.currentThread().getStackTrace();
+        for (StackTraceElement ste : st) {
+            if (ste.getClassName().startsWith("com.sun.javatest.")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static Path createWorkDirName(TestDesc testDesc) {
+        Path result = Path.of(".");
+        if (!isCalledByJavatest()) {
+            result = result.resolve(testDesc.clazz.getSimpleName());
+        }
+
+        List<String> components = new ArrayList<>();
+
+        final String testFunctionName = testDesc.functionName;
+        if (testFunctionName != null) {
+            components.add(testFunctionName);
+        }
+
+        final boolean isPrametrized = Stream.of(testDesc.functionArgs,
+                testDesc.instanceArgs).anyMatch(Objects::nonNull);
+        if (isPrametrized) {
+            components.add(String.format("%08x", testDesc.testFullName().hashCode()));
+        }
+
+        if (!components.isEmpty()) {
+            result = result.resolve(String.join(".", components));
+        }
+
+        return result;
+    }
+
     private enum Status {
         Passed("[       OK ]"),
         Failed("[  FAILED  ]"),
@@ -300,6 +317,7 @@
     private final List<ThrowingConsumer> beforeActions;
     private final List<ThrowingConsumer> afterActions;
     private final boolean dryRun;
+    private final Path workDir;
 
     private final static Set<Status> KEEP_WORK_DIR = Functional.identity(
             () -> {
--- a/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java	Fri Oct 18 14:14:37 2019 -0400
@@ -154,7 +154,7 @@
 
     private AppImageFile createFromXml(String... xmlData) throws IOException {
         Path directory = tempFolder.getRoot().toPath();
-        Path path = directory.resolve(AppImageFile.XML_FILENAME);
+        Path path = directory.resolve(AppImageFile.FILENAME);
         path.toFile().mkdirs();
         Files.delete(path);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/CompareDottedVersionTest.java	Fri Oct 18 14:14:37 2019 -0400
@@ -0,0 +1,84 @@
+/*
+ * 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.internal;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import junit.framework.*;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import static org.junit.Assert.*;
+
+@RunWith(Parameterized.class)
+public class CompareDottedVersionTest {
+
+    public CompareDottedVersionTest(String version1, String version2, int result) {
+        this.version1 = version1;
+        this.version2 = version2;
+        this.expectedResult = result;
+    }
+
+    @Parameters
+    public static List<Object[]> data() {
+        return List.of(new Object[][] {
+            { "00.0.0", "0", 0 },
+            { "0.035", "0.0035", 0 },
+            { "1", "1", 0 },
+            { "2", "2.0", 0 },
+            { "2.00", "2.0", 0 },
+            { "1.2.3.4", "1.2.3.4.5", -1 },
+            { "34", "33", 1 },
+            { "34.0.78", "34.1.78", -1 }
+        });
+    }
+
+    @Test
+    public void testIt() {
+        int actualResult = compare(version1, version2);
+        assertEquals(expectedResult, actualResult);
+
+        int actualNegateResult = compare(version2, version1);
+        assertEquals(actualResult, -1 * actualNegateResult);
+    }
+
+    private static int compare(String x, String y) {
+        int result = new DottedVersion(x).compareTo(y);
+
+        if (result < 0) {
+            return -1;
+        }
+
+        if (result > 0) {
+            return 1;
+        }
+
+        return 0;
+    }
+
+    private final String version1;
+    private final String version2;
+    private final int expectedResult;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/DottedVersionTest.java	Fri Oct 18 14:14:37 2019 -0400
@@ -0,0 +1,64 @@
+/*
+ * 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.internal;
+
+import java.util.stream.Stream;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import static org.junit.Assert.*;
+
+public class DottedVersionTest {
+
+    @Rule
+    public ExpectedException exceptionRule = ExpectedException.none();
+
+    @Test
+    public void testValid() {
+        Stream.of(
+            "1.0",
+            "1",
+            "2.234.045",
+            "2.234.0",
+            "0",
+            "0.1"
+        ).forEach(value -> {
+            DottedVersion version = new DottedVersion(value);
+            assertEquals(version.toString(), value);
+        });
+    }
+
+    @Test
+    public void testNull() {
+        exceptionRule.expect(NullPointerException.class);
+        new DottedVersion(null);
+    }
+
+    @Test
+    public void testEmpty() {
+        exceptionRule.expect(IllegalArgumentException.class);
+        exceptionRule.expectMessage("Version may not be empty string");
+        new DottedVersion("");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/InvalidDottedVersionTest.java	Fri Oct 18 14:14:37 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.internal;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InvalidDottedVersionTest {
+
+    public InvalidDottedVersionTest(String version) {
+        this.version = version;
+    }
+
+    @Parameters
+    public static List<Object[]> data() {
+        return Stream.of(
+            "1.-1",
+            "5.",
+            "4.2.",
+            "3..2",
+            "2.a",
+            "0a",
+            ".",
+            " ",
+            " 1",
+            "1. 2"
+        ).map(version -> new Object[] { version }).collect(Collectors.toList());
+    }
+
+    @Rule
+    public ExpectedException exceptionRule = ExpectedException.none();
+
+    @Test
+    public void testIt() {
+        exceptionRule.expect(IllegalArgumentException.class);
+        new DottedVersion(version);
+    }
+
+    private final String version;
+}
--- a/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/PathGroupTest.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/PathGroupTest.java	Fri Oct 18 14:14:37 2019 -0400
@@ -24,19 +24,27 @@
  */
 package jdk.jpackage.internal;
 
+import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.not;
 import static org.junit.Assert.*;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
 
 public class PathGroupTest {
-    public PathGroupTest() {
-    }
+
+    @Rule
+    public final TemporaryFolder tempFolder = new TemporaryFolder();
 
     @Test(expected = NullPointerException.class)
     public void testNullId() {
@@ -125,7 +133,139 @@
         assertEquals(aPath, pg2.roots().get(0));
     }
 
+    @Test
+    public void testTransform() throws IOException {
+        for (var transform : TransformType.values()) {
+            testTransform(false, transform);
+        }
+    }
+
+    @Test
+    public void testTransformWithExcludes() throws IOException {
+        for (var transform : TransformType.values()) {
+            testTransform(true, transform);
+        }
+    }
+
+    enum TransformType { Copy, Move, Handler };
+
+    private void testTransform(boolean withExcludes, TransformType transform)
+            throws IOException {
+        final PathGroup pg = new PathGroup(Map.of(0, PATH_FOO, 1, PATH_BAR, 2,
+                PATH_EMPTY, 3, PATH_BAZ));
+
+        final Path srcDir = tempFolder.newFolder().toPath();
+        final Path dstDir = tempFolder.newFolder().toPath();
+
+        Files.createDirectories(srcDir.resolve(PATH_FOO).resolve("a/b/c/d"));
+        Files.createFile(srcDir.resolve(PATH_FOO).resolve("a/b/c/file1"));
+        Files.createFile(srcDir.resolve(PATH_FOO).resolve("a/b/file2"));
+        Files.createFile(srcDir.resolve(PATH_FOO).resolve("a/b/file3"));
+        Files.createFile(srcDir.resolve(PATH_BAR));
+        Files.createFile(srcDir.resolve(PATH_EMPTY).resolve("file4"));
+        Files.createDirectories(srcDir.resolve(PATH_BAZ).resolve("1/2/3"));
+
+        var dst = pg.resolveAt(dstDir);
+        var src = pg.resolveAt(srcDir);
+        if (withExcludes) {
+            // Exclude from transformation.
+            src.setPath(new Object(), srcDir.resolve(PATH_FOO).resolve("a/b/c"));
+            src.setPath(new Object(), srcDir.resolve(PATH_EMPTY).resolve("file4"));
+        }
+
+        var srcFilesBeforeTransform = walkFiles(srcDir);
+
+        if (transform == TransformType.Handler) {
+            List<Map.Entry<Path, Path>> copyFile = new ArrayList<>();
+            List<Path> createDirectory = new ArrayList<>();
+            src.transform(dst, new PathGroup.TransformHandler() {
+                @Override
+                public void copyFile(Path src, Path dst) throws IOException {
+                    copyFile.add(Map.entry(src, dst));
+                }
+
+                @Override
+                public void createDirectory(Path dir) throws IOException {
+                    createDirectory.add(dir);
+                }
+            });
+
+            Consumer<Path> assertFile = path -> {
+                var entry = Map.entry(srcDir.resolve(path), dstDir.resolve(path));
+                assertTrue(copyFile.contains(entry));
+            };
+
+            Consumer<Path> assertDir = path -> {
+                assertTrue(createDirectory.contains(dstDir.resolve(path)));
+            };
+
+            assertEquals(withExcludes ? 3 : 5, copyFile.size());
+            assertEquals(withExcludes ? 8 : 10, createDirectory.size());
+
+            assertFile.accept(PATH_FOO.resolve("a/b/file2"));
+            assertFile.accept(PATH_FOO.resolve("a/b/file3"));
+            assertFile.accept(PATH_BAR);
+            assertDir.accept(PATH_FOO.resolve("a/b"));
+            assertDir.accept(PATH_FOO.resolve("a"));
+            assertDir.accept(PATH_FOO);
+            assertDir.accept(PATH_BAZ);
+            assertDir.accept(PATH_BAZ.resolve("1"));
+            assertDir.accept(PATH_BAZ.resolve("1/2"));
+            assertDir.accept(PATH_BAZ.resolve("1/2/3"));
+            assertDir.accept(PATH_EMPTY);
+
+            if (!withExcludes) {
+                assertFile.accept(PATH_FOO.resolve("a/b/c/file1"));
+                assertFile.accept(PATH_EMPTY.resolve("file4"));
+                assertDir.accept(PATH_FOO.resolve("a/b/c/d"));
+                assertDir.accept(PATH_FOO.resolve("a/b/c"));
+            }
+
+            assertArrayEquals(new Path[] { Path.of("") }, walkFiles(dstDir));
+            return;
+        }
+
+        if (transform == TransformType.Copy) {
+            src.copy(dst);
+        } else if (transform == TransformType.Move) {
+            src.move(dst);
+        }
+
+        final List<Path> excludedPaths;
+        if (withExcludes) {
+            excludedPaths = List.of(
+                PATH_EMPTY.resolve("file4"),
+                PATH_FOO.resolve("a/b/c")
+            );
+        } else {
+            excludedPaths = Collections.emptyList();
+        }
+        UnaryOperator<Path[]> removeExcludes = paths -> {
+            return Stream.of(paths)
+                    .filter(path -> !excludedPaths.stream().anyMatch(
+                            path::startsWith))
+                    .collect(Collectors.toList()).toArray(Path[]::new);
+        };
+
+        var dstFiles = walkFiles(dstDir);
+        assertArrayEquals(removeExcludes.apply(srcFilesBeforeTransform), dstFiles);
+
+        if (transform == TransformType.Copy) {
+            assertArrayEquals(dstFiles, removeExcludes.apply(walkFiles(srcDir)));
+        } else if (transform == TransformType.Move) {
+            assertFalse(Files.exists(srcDir));
+        }
+    }
+
+    private static Path[] walkFiles(Path root) throws IOException {
+        try (var files = Files.walk(root)) {
+            return files.map(root::relativize).sorted().collect(
+                    Collectors.toList()).toArray(Path[]::new);
+        }
+    }
+
     private final static Path PATH_FOO = Path.of("foo");
     private final static Path PATH_BAR = Path.of("bar");
+    private final static Path PATH_BAZ = Path.of("baz");
     private final static Path PATH_EMPTY = Path.of("");
 }
--- a/test/jdk/tools/jpackage/share/FileAssociationsTest.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/share/FileAssociationsTest.java	Fri Oct 18 14:14:37 2019 -0400
@@ -21,6 +21,7 @@
  * questions.
  */
 
+import java.nio.file.Path;
 import jdk.jpackage.test.TKit;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.FileAssociations;
@@ -42,6 +43,11 @@
  * 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.
+ *
+ * Icon associated with the main launcher should be associated with files with
+ * ".jptest1" suffix. Different icon should be associated with files with with
+ * ".jptest2" suffix. Icon for files with ".jptest1" suffix is platform specific
+ * and is one of 'icon.*' files in test/jdk/tools/jpackage/resources directory.
  */
 
 /*
@@ -60,8 +66,12 @@
         PackageTest packageTest = new PackageTest();
 
         applyFileAssociations(packageTest, new FileAssociations("jptest1"));
+
+        Path icon = TKit.TEST_SRC_ROOT.resolve(Path.of("resources", "icon"
+                + TKit.ICON_SUFFIX));
+
         applyFileAssociations(packageTest,
-                new FileAssociations("jptest2").setFilename("fa2"));
+                new FileAssociations("jptest2").setFilename("fa2").setIcon(icon));
         packageTest.run();
     }
 
--- a/test/jdk/tools/jpackage/share/IconTest.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/share/IconTest.java	Fri Oct 18 14:14:37 2019 -0400
@@ -25,6 +25,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
+import jdk.jpackage.internal.IOUtils;
 import jdk.jpackage.test.TKit;
 import jdk.jpackage.test.Functional;
 import jdk.jpackage.test.Annotations.*;
@@ -70,8 +71,8 @@
     }
 
     private static String appIconFileName(JPackageCommand cmd) {
-        return cmd.appLauncherPath().getFileName().toString().replaceAll(
-                "\\.[^.]*$", "") + ICON_SUFFIX;
+        return IOUtils.replaceSuffix(cmd.appLauncherPath().getFileName(),
+                TKit.ICON_SUFFIX).toString();
     }
 
     private static void testIt(JPackageCommand cmd) throws IOException {
@@ -87,22 +88,6 @@
                         iconPath, GOLDEN_ICON));
     }
 
-    private final static String ICON_SUFFIX = Functional.identity(() -> {
-        if (TKit.isOSX()) {
-            return ".icns";
-        }
-
-        if (TKit.isLinux()) {
-            return ".png";
-        }
-
-        if (TKit.isWindows()) {
-            return ".ico";
-        }
-
-        throw TKit.throwUnknownPlatformError();
-    }).get();
-
     private final static Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of(
-            "resources", "icon" + ICON_SUFFIX));
+            "resources", "icon" + TKit.ICON_SUFFIX));
 }
--- a/test/jdk/tools/jpackage/share/RuntimePackageTest.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/share/RuntimePackageTest.java	Fri Oct 18 14:14:37 2019 -0400
@@ -48,7 +48,7 @@
  * @comment Temporary disable for Linux and OSX until functionality implemented
  * @requires (os.family != "mac")
  * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm/timeout=360 -Xmx512m RuntimePackageTest
+ * @run main/othervm/timeout=720 -Xmx512m RuntimePackageTest
  */
 public class RuntimePackageTest {
 
--- a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java	Fri Oct 18 11:00:57 2019 -0400
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/MainClassTest.java	Fri Oct 18 14:14:37 2019 -0400
@@ -53,7 +53,6 @@
  * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
  *  --jpt-run=jdk.jpackage.tests.MainClassTest
  *  --jpt-space-subst=_
- *  --jpt-exclude=modular=y;_main-class=n;_jar-main-class=b;_jlink=y
  */
 
 public final class MainClassTest {
@@ -64,7 +63,7 @@
         }
 
         Script modular(boolean v) {
-            appDesc.setModuleName(v ? null : "com.other");
+            appDesc.setModuleName(v ? "com.other" : null);
             return this;
         }
 
@@ -138,9 +137,12 @@
                 script.appDesc.packageName(), "ThereIsNoSuchClass").filter(
                 Objects::nonNull).collect(Collectors.joining("."));
 
-        cmd = JPackageCommand.helloAppImage(script.appDesc);
+        cmd = JPackageCommand
+                .helloAppImage(script.appDesc)
+                .ignoreDefaultRuntime(true);
         if (!script.withJLink) {
-            cmd.setFakeRuntime();
+            cmd.addArguments("--runtime-image", Path.of(System.getProperty(
+                    "java.home")));
         }
 
         final String moduleName = script.appDesc.moduleName();
@@ -173,13 +175,6 @@
             for (var modular : List.of(true, false)) {
                 for (var mainClass : Script.MainClassType.values()) {
                     for (var jarMainClass : Script.MainClassType.values()) {
-                        if (!withJLink && (jarMainClass == SetWrong || mainClass
-                                == SetWrong)) {
-                            // Without runtime can't run app to verify it will fail, so
-                            // there is no point in such testing.
-                            continue;
-                        }
-
                         Script script = new Script()
                             .modular(modular)
                             .withJLink(withJLink)
@@ -190,10 +185,10 @@
                                 || withMainClass.contains(mainClass)) {
                         } else if (modular) {
                             script.expectedErrorMessage(
-                                    "A main class was not specified nor was one found in the jar");
+                                    "Error: Main application class is missing");
                         } else {
                             script.expectedErrorMessage(
-                                    "Error: Main application class is missing");
+                                    "A main class was not specified nor was one found in the jar");
                         }
 
                         scripts.add(new Script[]{script});
@@ -250,13 +245,13 @@
         }
     }
 
-    private void initJarWithWrongMainClass() {
+    private void initJarWithWrongMainClass() throws IOException {
         // Call JPackageCommand.executePrerequisiteActions() to build app's jar.
         // executePrerequisiteActions() is called by JPackageCommand instance
         // only once.
         cmd.executePrerequisiteActions();
 
-        Path jarFile;
+        final Path jarFile;
         if (script.appDesc.moduleName() != null) {
             jarFile = Path.of(cmd.getArgumentValue("--module-path"),
                     script.appDesc.jarFileName());
@@ -264,37 +259,58 @@
             jarFile = cmd.inputDir().resolve(cmd.getArgumentValue("--main-jar"));
         }
 
-        // Create jar file with the main class attribute in manifest set to
-        // non-existing class.
+        // Create new jar file filtering out main class from the old jar file.
         TKit.withTempDirectory("repack-jar", workDir -> {
-            Path manifestFile = workDir.resolve("META-INF/MANIFEST.MF");
-            try (var jar = new JarFile(jarFile.toFile())) {
-                jar.stream()
-                .filter(Predicate.not(JarEntry::isDirectory))
-                .sequential().forEachOrdered(ThrowingConsumer.toConsumer(
-                    jarEntry -> {
-                        try (var in = jar.getInputStream(jarEntry)) {
-                            Path fileName = workDir.resolve(jarEntry.getName());
-                            Files.createDirectories(fileName.getParent());
-                            Files.copy(in, fileName);
-                        }
-                    }));
-            }
+            // Extract app's class from the old jar.
+            explodeJar(jarFile, workDir,
+                    jarEntry -> Path.of(jarEntry.getName()).equals(
+                            script.appDesc.classFilePath()));
+
+            // Create app's jar file with different main class.
+            var badAppDesc = JavaAppDesc.parse(script.appDesc.toString()).setClassName(
+                    nonExistingMainClass);
+            JPackageCommand.helloAppImage(badAppDesc).executePrerequisiteActions();
+
+            // Extract new jar but skip app's class.
+            explodeJar(jarFile, workDir,
+                    jarEntry -> !Path.of(jarEntry.getName()).equals(
+                            badAppDesc.classFilePath()));
+
+            // At this point we should have:
+            // 1. Manifest from the new jar referencing non-existing class
+            //  as the main class.
+            // 2. Module descriptor referencing non-existing class as the main
+            //  class in case of modular app.
+            // 3. App's class from the old jar. We need it to let jlink find some
+            //  classes in the package declared in module descriptor
+            //  in case of modular app.
 
             Files.delete(jarFile);
-
-            // Adjust manifest.
-            TKit.createTextFile(manifestFile, Files.readAllLines(
-                    manifestFile).stream().map(line -> line.replace(
-                    script.appDesc.className(), nonExistingMainClass)));
-
             new Executor().setToolProvider(JavaTool.JAR)
-            .addArguments("-c", "-M", "-f", jarFile.toString())
+            .addArguments("-v", "-c", "-M", "-f", jarFile.toString())
             .addArguments("-C", workDir.toString(), ".")
+            .dumpOutput()
             .execute().assertExitCodeIsZero();
         });
     }
 
+    private static void explodeJar(Path jarFile, Path workDir,
+            Predicate<JarEntry> filter) throws IOException {
+        try (var jar = new JarFile(jarFile.toFile())) {
+            jar.stream()
+            .filter(Predicate.not(JarEntry::isDirectory))
+            .filter(filter)
+            .sequential().forEachOrdered(ThrowingConsumer.toConsumer(
+                jarEntry -> {
+                    try (var in = jar.getInputStream(jarEntry)) {
+                        Path fileName = workDir.resolve(jarEntry.getName());
+                        Files.createDirectories(fileName.getParent());
+                        Files.copy(in, fileName);
+                    }
+                }));
+        }
+    }
+
     private final JPackageCommand cmd;
     private final Script script;
     private final String nonExistingMainClass;