8230920 : jpackage problems when -input dir contains any files with "cfg" extension. JDK-8200758-branch
authorherrick
Mon, 30 Sep 2019 15:13:14 -0400
branchJDK-8200758-branch
changeset 58414 a5f66aa04f68
parent 58360 fd45b7e2c027
child 58415 73f8e557549a
8230920 : jpackage problems when -input dir contains any files with "cfg" extension. Reviewed-by: asemenyuk, almatvee
src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinAppBundler.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java
test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java
test/jdk/tools/jpackage/junit/junit.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java	Mon Sep 30 15:13:14 2019 -0400
@@ -0,0 +1,218 @@
+/*
+ * 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;
+    }
+    
+}
--- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinAppBundler.java	Thu Sep 26 10:37:37 2019 -0400
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinAppBundler.java	Mon Sep 30 15:13:14 2019 -0400
@@ -98,61 +98,6 @@
         return true;
     }
 
-    private static boolean usePredefineAppName(Map<String, ? super Object> p) {
-        return (PREDEFINED_APP_IMAGE.fetchFrom(p) != null);
-    }
-
-    private static String appName;
-    synchronized static String getAppName(
-            Map<String, ? super Object> p) {
-        // If we building from predefined app image, then we should use names
-        // from image and not from CLI.
-        if (usePredefineAppName(p)) {
-            if (appName == null) {
-                // Use WIN_APP_IMAGE here, since we already copy pre-defined
-                // image to WIN_APP_IMAGE
-                File appImageDir = WIN_APP_IMAGE.fetchFrom(p);
-
-                File appDir = new File(appImageDir.toString() + "\\app");
-                File [] files = appDir.listFiles(
-                        (File dir, String name) -> name.endsWith(".cfg"));
-                if (files == null || files.length == 0) {
-                    String name = APP_NAME.fetchFrom(p);
-                    Path exePath = appImageDir.toPath().resolve(name + ".exe");
-                    Path icoPath = appImageDir.toPath().resolve(name + ".ico");
-                    if (exePath.toFile().exists() &&
-                            icoPath.toFile().exists()) {
-                        return name;
-                    } else {
-                        throw new RuntimeException(MessageFormat.format(
-                                I18N.getString("error.cannot-find-launcher"),
-                                appImageDir));
-                    }
-                } else {
-                    appName = files[0].getName();
-                    int index = appName.indexOf(".");
-                    if (index != -1) {
-                        appName = appName.substring(0, index);
-                    }
-                    if (files.length > 1) {
-                        Log.error(MessageFormat.format(I18N.getString(
-                                "message.multiple-launchers"), appName));
-                    }
-                }
-                return appName;
-            } else {
-                return appName;
-            }
-        }
-
-        return APP_NAME.fetchFrom(p);
-    }
-
-    public static String getLauncherRelativePath(
-            Map<String, ? super Object> p) {
-        return getAppName(p) + ".exe";
-    }
-
     public boolean bundle(Map<String, ? super Object> p, File outputDirectory)
             throws PackagerException {
         return doBundle(p, outputDirectory, false) != null;
--- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java	Thu Sep 26 10:37:37 2019 -0400
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java	Mon Sep 30 15:13:14 2019 -0400
@@ -542,6 +542,25 @@
                 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 {
 
@@ -558,39 +577,29 @@
             xml.writeStartDocument();
             xml.writeStartElement("Include");
 
-            File launcher = new File(imageRootDir,
-                    WinAppBundler.getLauncherRelativePath(params));
-            if (launcher.exists()) {
-                String iconPath = launcher.getAbsolutePath().replace(
-                        ".exe", ".ico");
-                if (MENU_HINT.fetchFrom(params)) {
-                    xml.writeStartElement("Icon");
-                    xml.writeAttribute("Id", "StartMenuIcon.exe");
-                    xml.writeAttribute("SourceFile", iconPath);
-                    xml.writeEndElement();
-                }
-                if (SHORTCUT_HINT.fetchFrom(params)) {
-                    xml.writeStartElement("Icon");
-                    xml.writeAttribute("Id", "DesktopIcon.exe");
-                    xml.writeAttribute("SourceFile", iconPath);
-                    xml.writeEndElement();
-                }
+            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 < addLaunchers.size(); i++) {
-                Map<String, ? super Object> sl = addLaunchers.get(i);
-                if (SHORTCUT_HINT.fetchFrom(sl) || MENU_HINT.fetchFrom(sl)) {
-                    File addLauncher = new File(imageRootDir,
-                            WinAppBundler.getLauncherRelativePath(sl));
-                    String addLauncherPath
-                            = relativePath(imageRootDir, addLauncher);
-                    String addLauncherIconPath
-                            = addLauncherPath.replace(".exe", ".ico");
+            for (int i = 0; i < icons.length; i++) {
+                if (icons[i].exists()) {
+                    String iconPath = icons[i].getAbsolutePath();
 
-                    xml.writeStartElement("Icon");
-                    xml.writeAttribute("Id", "Launcher" + i + ".exe");
-                    xml.writeAttribute("SourceFile", addLauncherIconPath);
-                    xml.writeEndElement();
+                    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();
+                    }
                 }
             }
 
@@ -718,21 +727,16 @@
         out.println(prefix + "  <RemoveFolder Id=\"RemoveDir"
                 + (id++) + "\" On=\"uninstall\" />");
 
-        boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params);
+
         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
-        File launcherFile = new File(imageRootDir,
-                WinAppBundler.getLauncherRelativePath(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
-
-        for (File f: files) {
-            boolean isLauncher = f.equals(launcherFile);
-            if (isLauncher) {
-                needRegistryKey = true;
-            }
-        }
+        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
@@ -748,21 +752,23 @@
             out.println(prefix + "   </RegistryKey>");
         }
 
-        boolean menuShortcut = MENU_HINT.fetchFrom(params);
-        boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
+        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 isLauncher = f.equals(launcherFile);
-
-            launcherSet = launcherSet || isLauncher;
+            boolean isMainLauncher =
+                    launcherFiles.length > 0 && f.equals(launcherFiles[0]);
 
-            boolean doShortcuts =
-                isLauncher && (menuShortcut || desktopShortcut);
+            launcherSet = launcherSet || isMainLauncher;
 
-            String thisFileId = isLauncher ? LAUNCHER_ID : ("FileId" + (id++));
+            String thisFileId = isMainLauncher ? LAUNCHER_ID : ("FileId" + (id++));
             idToFileMap.put(f.getName(), thisFileId);
 
             out.println(prefix + "   <File Id=\"" +
@@ -770,48 +776,46 @@
                     + " Name=\"" + f.getName() + "\" "
                     + " Source=\"" + relativePath(imageRootDir, f) + "\""
                     + " ProcessorArchitecture=\"x64\"" + ">");
-            if (doShortcuts && desktopShortcut) {
+            if (isMainLauncher && desktopShortcut) {
                 out.println(prefix
                         + "  <Shortcut Id=\"desktopShortcut\" Directory="
                         + "\"DesktopFolder\""
-                        + " Name=\"" + APP_NAME.fetchFrom(params)
+                        + " Name=\"" + launcherNames[0]
                         + "\" WorkingDirectory=\"INSTALLDIR\""
-                        + " Advertise=\"no\" Icon=\"DesktopIcon.exe\""
+                        + " Advertise=\"no\" Icon=\"DesktopIcon.exe0\""
                         + " IconIndex=\"0\" />");
             }
-            if (doShortcuts && menuShortcut) {
+            if (isMainLauncher && menuShortcut) {
                 out.println(prefix
                         + "     <Shortcut Id=\"ExeShortcut\" Directory="
                         + "\"ProgramMenuDir\""
-                        + " Name=\"" + APP_NAME.fetchFrom(params)
-                        + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe\""
+                        + " Name=\"" + launcherNames[0]
+                        + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe0\""
                         + " IconIndex=\"0\" />");
             }
 
-            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);
-                File addLauncherFile = new File(imageRootDir,
-                        WinAppBundler.getLauncherRelativePath(sl));
-                if (f.equals(addLauncherFile)) {
-                    if (SHORTCUT_HINT.fetchFrom(sl)) {
+            // any additional launchers
+            for (int index = 1; index < launcherNames.length; index++ ) {
+
+                if (f.equals(launcherFiles[index])) {
+                    if (desktopShortcut) {
                         out.println(prefix
                                 + "  <Shortcut Id=\"desktopShortcut"
-                                + i + "\" Directory=\"DesktopFolder\""
-                                + " Name=\"" + APP_NAME.fetchFrom(sl)
+                                + index + "\" Directory=\"DesktopFolder\""
+                                + " Name=\"" + launcherNames[index]
                                 + "\" WorkingDirectory=\"INSTALLDIR\""
-                                + " Advertise=\"no\" Icon=\"Launcher"
-                                + i + ".exe\" IconIndex=\"0\" />");
+                                + " Advertise=\"no\" Icon=\"DesktopIcon.exe"
+                                + index + "\""
+                                + " IconIndex=\"0\" />");
                     }
-                    if (MENU_HINT.fetchFrom(sl)) {
+                    if (menuShortcut) {
                         out.println(prefix
-                                + "     <Shortcut Id=\"ExeShortcut"
-                                + i + "\" Directory=\"ProgramMenuDir\""
-                                + " Name=\"" + APP_NAME.fetchFrom(sl)
-                                + "\" Advertise=\"no\" Icon=\"Launcher"
-                                + i + ".exe\" IconIndex=\"0\" />");
-                        // Should we allow different menu groups?  Not for now.
+                            + "     <Shortcut Id=\"ExeShortcut"
+                            + index + "\" Directory=\"ProgramMenuDir\""
+                            + " Name=\"" + launcherNames[index]
+                            + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe"
+                            + index + "\""
+                            + " IconIndex=\"0\" />");
                     }
                 }
             }
--- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java	Thu Sep 26 10:37:37 2019 -0400
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java	Mon Sep 30 15:13:14 2019 -0400
@@ -187,6 +187,7 @@
         } catch (PackagerException pe) {
             throw new RuntimeException(pe);
         }
+        AppImageFile.save(root, params);
 
         // create the .exe launchers
         createLauncherForEntryPoint(params);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/AppImageFileTest.java	Mon Sep 30 15:13:14 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.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+public class AppImageFileTest {
+
+    @Rule
+    public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+    @Test
+    public void testIdentity() throws IOException {
+        Map<String, ? super Object> params = new LinkedHashMap<>();
+        params.put("name", "Foo");
+        params.put("app-version", "2.3");
+        params.put("description", "Duck is the King");
+        AppImageFile aif = create(params);
+
+        Assert.assertEquals("Foo", aif.getLauncherName());
+    }
+
+    @Test
+    public void testInvalidCommandLine() throws IOException {
+        // Just make sure AppImageFile will tolerate jpackage params that would
+        // never create app image at both load/save phases.
+        // People would edit this file just because they can.
+        // We should be ready to handle curious minds.
+        Map<String, ? super Object> params = new LinkedHashMap<>();
+        params.put("invalidParamName", "randomStringValue");
+        create(params);
+
+        params = new LinkedHashMap<>();
+        params.put("name", "foo");
+        params.put("app-version", "");
+        create(params);
+    }
+
+    @Test
+    public void testInavlidXml() throws IOException {
+        assertInvalid(createFromXml("<foo/>"));
+        assertInvalid(createFromXml("<jpackage-state/>"));
+        assertInvalid(createFromXml(
+                "<jpackage-state>",
+                    "<main-launcher></main-launcher>",
+                "</jpackage-state>"));
+    }
+
+    @Test
+    public void testValidXml() throws IOException {
+        Assert.assertEquals("Foo", (createFromXml(
+                "<jpackage-state>",
+                    "<main-launcher>Foo</main-launcher>",
+                "</jpackage-state>")).getLauncherName());
+
+        Assert.assertEquals("Foo", (createFromXml(
+                "<jpackage-state>",
+                    "<main-launcher>Foo</main-launcher>",
+                    "<main-launcher>Bar</main-launcher>",
+                "</jpackage-state>")).getLauncherName());
+
+        var file = createFromXml(
+                "<jpackage-state>",
+                    "<main-launcher>Foo</main-launcher>",
+                    "<launcher></launcher>",
+                "</jpackage-state>");
+        Assert.assertEquals("Foo", file.getLauncherName());
+        Assert.assertArrayEquals(new String[0],
+                file.getAddLauncherNames().toArray(String[]::new));
+    }
+
+    @Test
+    public void testMainLauncherName() throws IOException {
+        Map<String, ? super Object> params = new LinkedHashMap<>();
+        params.put("name", "Foo");
+        params.put("description", "Duck App Description");
+        AppImageFile aif = create(params);
+
+        Assert.assertEquals("Foo", aif.getLauncherName());
+    }
+
+    @Test
+    public void testAddLauncherNames() throws IOException {
+    }
+
+    private AppImageFile create(Map<String, Object> params) throws IOException {
+        AppImageFile.save(tempFolder.getRoot().toPath(), params);
+        return AppImageFile.load(tempFolder.getRoot().toPath());
+    }
+
+    private void assertInvalid(AppImageFile file) {
+        Assert.assertNull(file.getLauncherName());
+        Assert.assertNull(file.getAddLauncherNames());
+    }
+
+    private AppImageFile createFromXml(String... xmlData) throws IOException {
+        Path directory = tempFolder.getRoot().toPath();
+        Path path = directory.resolve(AppImageFile.XML_FILENAME);
+        path.toFile().mkdirs();
+        Files.delete(path);
+
+        ArrayList<String> data = new ArrayList();
+        data.add("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>");
+        data.addAll(List.of(xmlData));
+
+        Files.write(path, data, StandardOpenOption.CREATE,
+                    StandardOpenOption.TRUNCATE_EXISTING);
+
+        AppImageFile image = AppImageFile.load(directory);
+        return image;
+    }
+
+}
--- a/test/jdk/tools/jpackage/junit/junit.java	Thu Sep 26 10:37:37 2019 -0400
+++ b/test/jdk/tools/jpackage/junit/junit.java	Mon Sep 30 15:13:14 2019 -0400
@@ -1,3 +1,28 @@
+/*
+ * 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.
+ */
+
 /*
  * @test
  * @summary jpackage unit tests
@@ -6,4 +31,5 @@
  *  jdk.jpackage.internal.PathGroupTest
  *  jdk.jpackage.internal.DeployParamsTest
  *  jdk.jpackage.internal.ApplicationLayoutTest
+ *  jdk.jpackage.internal.AppImageFileTest
  */