src/demo/share/jpackager/JNLPConverter/src/jnlp/converter/parser/XMLFormat.java
branchJDK-8200758-branch
changeset 56963 eaca4369b068
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/demo/share/jpackager/JNLPConverter/src/jnlp/converter/parser/XMLFormat.java	Fri Oct 12 19:00:51 2018 -0400
@@ -0,0 +1,659 @@
+/*
+ * Copyright (c) 2006, 2018, 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 jnlp.converter.parser;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.ArrayList;
+import jnlp.converter.JNLPConverter;
+import jnlp.converter.parser.exception.MissingFieldException;
+import jnlp.converter.parser.exception.BadFieldException;
+import jnlp.converter.parser.exception.JNLParseException;
+import jnlp.converter.parser.xml.XMLEncoding;
+import jnlp.converter.parser.xml.XMLParser;
+import jnlp.converter.parser.xml.XMLNode;
+import jnlp.converter.parser.JNLPDesc.AssociationDesc;
+import jnlp.converter.parser.JNLPDesc.IconDesc;
+import jnlp.converter.parser.JNLPDesc.InformationDesc;
+import jnlp.converter.parser.JNLPDesc.ShortcutDesc;
+import jnlp.converter.parser.ResourcesDesc.JARDesc;
+import jnlp.converter.parser.ResourcesDesc.JREDesc;
+import jnlp.converter.Log;
+import jnlp.converter.parser.ResourcesDesc.ExtensionDesc;
+import jnlp.converter.parser.ResourcesDesc.PropertyDesc;
+import org.xml.sax.SAXParseException;
+
+public class XMLFormat {
+
+    public static XMLNode parseBits(byte[] bits) throws JNLParseException {
+        return parse(decode(bits));
+    }
+
+    private static String decode(byte[] bits) throws JNLParseException {
+        try {
+            return XMLEncoding.decodeXML(bits);
+        } catch (Exception e) {
+            throw new JNLParseException(e,
+                "exception determining encoding of jnlp file", 0);
+        }
+    }
+
+    private static XMLNode parse(String source) throws JNLParseException {
+        try {
+            return (new XMLParser(source).parse());
+        } catch (SAXParseException spe) {
+            throw new JNLParseException(spe,
+                        "exception parsing jnlp file", spe.getLineNumber());
+        } catch (Exception e) {
+            throw new JNLParseException(e,
+                        "exception parsing jnlp file", 0);
+        }
+    }
+
+    /**
+     * thisCodebase, if set, is used to determine the codebase,
+     *     if JNLP codebase is not absolute.
+     *
+     * @param thisCodebase base URL of this JNLPDesc location
+     */
+    public static JNLPDesc parse(byte[] bits, URL thisCodebase, String jnlp)
+            throws Exception {
+
+        JNLPDesc jnlpd = new JNLPDesc();
+        String source = decode(bits).trim();
+        XMLNode root = parse(source);
+
+        if (root == null || root.getName() == null) {
+            throw new JNLParseException(null, null, 0);
+        }
+
+        // Check that root element is a <jnlp> tag
+        if (!root.getName().equals("jnlp")) {
+            throw (new MissingFieldException(source, "<jnlp>"));
+        }
+
+        // Read <jnlp> attributes (path is empty, i.e., "")
+        // (spec, version, codebase, href)
+        String specVersion = XMLUtils.getAttribute(root, "", "spec", "1.0+");
+        jnlpd.setSpecVersion(specVersion);
+        String version = XMLUtils.getAttribute(root, "", "version");
+        jnlpd.setVersion(version);
+
+        // Make sure the codebase URL ends with a '/'.
+        //
+        // Regarding the JNLP spec,
+        // the thisCodebase is used to determine the codebase.
+        //      codebase = new URL(thisCodebase, codebase)
+        URL codebase = GeneralUtil.asPathURL(XMLUtils.getAttributeURL(source,
+            thisCodebase, root, "", "codebase"));
+        if (codebase == null && thisCodebase != null) {
+            codebase = thisCodebase;
+        }
+        jnlpd.setCodebase(codebase.toExternalForm());
+
+        // Get href for JNLP file
+        URL href = XMLUtils.getAttributeURL(source, codebase, root, "", "href");
+        jnlpd.setHref(href.toExternalForm());
+
+        // Read <security> attributes
+        if (XMLUtils.isElementPath(root, "<security><all-permissions>")) {
+            jnlpd.setIsSandbox(false);
+        } else if (XMLUtils.isElementPath(root,
+                "<security><j2ee-application-client-permissions>")) {
+            jnlpd.setIsSandbox(false);
+        }
+
+        // We can be fxapp, and also be applet, or application, or neither
+        boolean isFXApp = false;
+        boolean isApplet = false;
+        if (XMLUtils.isElementPath(root, "<javafx-desc>")) {
+            // no new type for javafx-desc - needs one of the others
+            buildFXAppDesc(source, root, "<javafx-desc>", jnlpd);
+            jnlpd.setIsFXApp(true);
+            isFXApp = true;
+        }
+
+        /*
+         * Note - the jnlp specification says there must be exactly one of
+         * the descriptor types.  This code has always violated (or at least
+         * not checked for) that condition.
+         * Instead it uses precedent order app, component, installer, applet
+         * and ignores any other descriptors given.
+         */
+        if (XMLUtils.isElementPath(root, "<application-desc>")) {
+            buildApplicationDesc(source, root, jnlpd);
+        } else if (XMLUtils.isElementPath(root, "<component-desc>")) {
+            jnlpd.setIsLibrary(true);
+        } else if (XMLUtils.isElementPath(root, "<installer-desc>")) {
+            Log.warning("<installer-desc> is not supported and will be ignored in " + jnlp);
+            jnlpd.setIsInstaller(true);
+        } else if (XMLUtils.isElementPath(root, "<applet-desc>")) {
+            isApplet = true;
+        } else {
+            if (!isFXApp) {
+                throw (new MissingFieldException(source,
+                    "<jnlp>(<application-desc>|<applet-desc>|" +
+                    "<installer-desc>|<component-desc>)"));
+            }
+        }
+
+        if (isApplet && !isFXApp) {
+            Log.error("Applet based applications deployed with <applet-desc> element are not supported.");
+        }
+
+        if (!jnlpd.isLibrary() && !jnlpd.isInstaller()) {
+            buildInformationDesc(source, codebase, root, jnlpd);
+        }
+
+        if (!jnlpd.isInstaller()) {
+            buildResourcesDesc(source, codebase, root, false, jnlpd);
+        }
+
+        if (!jnlpd.isLibrary() && !jnlpd.isInstaller()) {
+            jnlpd.parseResourceDesc();
+        }
+
+        if (!jnlpd.isInstaller()) {
+            if (jnlpd.isSandbox()) {
+                if (jnlpd.isLibrary()) {
+                    Log.warning(jnlp + " is sandbox extension. JNLPConverter does not support sandbox environment and converted application will run without security manager.");
+                } else {
+                    Log.warning("This is sandbox Web-Start application. JNLPConverter does not support sandbox environment and converted application will run without security manager.");
+                }
+            }
+        }
+
+        return jnlpd;
+    }
+
+    /**
+     * Create a combine informationDesc in the two informationDesc.
+     * The information present in id1 overwrite the information present in id2
+     */
+    private static InformationDesc combineInformationDesc(
+                                   InformationDesc id1, InformationDesc id2) {
+        if (id1 == null) {
+            return id2;
+        }
+        if (id2 == null) {
+            return id1;
+        }
+
+        String t1 = id1.getTitle();
+        String title  = (t1 != null && t1.length() > 0) ?
+            t1 : id2.getTitle();
+        String v1 = id1.getVendor();
+        String vendor = (v1 != null && v1.length() > 0) ?
+            v1 : id2.getVendor();
+
+        /** Copy descriptions */
+        String[] descriptions = new String[InformationDesc.NOF_DESC];
+        for (int i = 0; i < descriptions.length; i++) {
+            descriptions[i] = (id1.getDescription(i) != null)
+                    ? id1.getDescription(i) : id2.getDescription(i);
+        }
+
+        /** Icons */
+        ArrayList<IconDesc> iconList = new ArrayList<>();
+        if (id2.getIcons() != null) {
+            iconList.addAll(Arrays.asList(id2.getIcons()));
+        }
+        if (id1.getIcons() != null) {
+            iconList.addAll(Arrays.asList(id1.getIcons()));
+        }
+        IconDesc[] icons = new IconDesc[iconList.size()];
+        icons = iconList.toArray(icons);
+
+        ShortcutDesc hints = (id1.getShortcut() != null) ?
+                             id1.getShortcut() : id2.getShortcut();
+
+        AssociationDesc[] asd = ( AssociationDesc[] ) addArrays(
+            (Object[])id1.getAssociations(), (Object[])id2.getAssociations());
+
+        return new InformationDesc(title,
+                                   vendor,
+                                   descriptions,
+                                   icons,
+                                   hints,
+                                   asd);
+    }
+
+    /** Extract data from <information> tag */
+    private static void buildInformationDesc(final String source, final URL codebase, XMLNode root, JNLPDesc jnlpd)
+        throws MissingFieldException, BadFieldException {
+        final ArrayList<InformationDesc> list = new ArrayList<>();
+
+        // Iterates over all <information> nodes ignoring the type
+        XMLUtils.visitElements(root,
+            "<information>", new XMLUtils.ElementVisitor() {
+            @Override
+            public void visitElement(XMLNode e) throws
+                BadFieldException, MissingFieldException {
+
+                // Check for right os, arch, and locale
+                String[] os = GeneralUtil.getStringList(
+                            XMLUtils.getAttribute(e, "", "os", null));
+                String[] arch = GeneralUtil.getStringList(
+                            XMLUtils.getAttribute(e, "", "arch", null));
+                String[] locale = GeneralUtil.getStringList(
+                            XMLUtils.getAttribute(e, "", "locale", null));
+                if (GeneralUtil.prefixMatchStringList(
+                                os, GeneralUtil.getOSFullName()) &&
+                    GeneralUtil.prefixMatchArch(arch) &&
+                    matchDefaultLocale(locale))
+                {
+                    // Title, vendor
+                    String title = XMLUtils.getElementContents(e, "<title>");
+                    String vendor = XMLUtils.getElementContents(e, "<vendor>");
+
+                    // Descriptions
+                    String[] descriptions =
+                                new String[InformationDesc.NOF_DESC];
+                    descriptions[InformationDesc.DESC_DEFAULT] =
+                        XMLUtils.getElementContentsWithAttribute(
+                        e, "<description>", "kind", "", null);
+                    descriptions[InformationDesc.DESC_ONELINE] =
+                        XMLUtils.getElementContentsWithAttribute(
+                        e, "<description>", "kind", "one-line", null);
+                    descriptions[InformationDesc.DESC_SHORT] =
+                        XMLUtils.getElementContentsWithAttribute(
+                        e, "<description>", "kind", "short", null);
+                    descriptions[InformationDesc.DESC_TOOLTIP] =
+                        XMLUtils.getElementContentsWithAttribute(
+                        e, "<description>", "kind", "tooltip", null);
+
+                    // Icons
+                    IconDesc[] icons = getIconDescs(source, codebase, e);
+
+                    // Shortcut hints
+                    ShortcutDesc shortcuts = getShortcutDesc(e);
+
+                    // Association hints
+                    AssociationDesc[] associations = getAssociationDesc(
+                                                        source, codebase, e);
+
+                    list.add(new InformationDesc(
+                        title, vendor, descriptions, icons,
+                        shortcuts, associations));
+                }
+            }
+        });
+
+        /* Combine all information desc. information in a single one for
+         * the current locale using the following priorities:
+         *   1. locale == language_country_variant
+         *   2. locale == lauguage_country
+         *   3. locale == lauguage
+         *   4. no or empty locale
+         */
+        InformationDesc normId = new InformationDesc(null, null, null, null, null, null);
+        for (InformationDesc id : list) {
+            normId = combineInformationDesc(id, normId);
+        }
+
+        jnlpd.setTitle(normId.getTitle());
+        jnlpd.setVendor(normId.getVendor());
+        jnlpd.setDescriptions(normId.getDescription());
+        jnlpd.setIcons(normId.getIcons());
+        jnlpd.setShortcuts(normId.getShortcut());
+        jnlpd.setAssociations(normId.getAssociations());
+    }
+
+    private static Object[] addArrays (Object[] a1, Object[] a2) {
+        if (a1 == null) {
+            return a2;
+        }
+        if (a2 == null) {
+            return a1;
+        }
+        ArrayList<Object> list = new ArrayList<>();
+        int i;
+        for (i=0; i<a1.length; list.add(a1[i++]));
+        for (i=0; i<a2.length; list.add(a2[i++]));
+        return list.toArray(a1);
+    }
+
+    public static boolean matchDefaultLocale(String[] localeStr) {
+        return GeneralUtil.matchLocale(localeStr, GeneralUtil.getDefaultLocale());
+    }
+
+    /** Extract data from <resources> tag. There is only one. */
+    static void buildResourcesDesc(final String source,
+            final URL codebase, XMLNode root, final boolean ignoreJres, JNLPDesc jnlpd)
+            throws MissingFieldException, BadFieldException {
+        // Extract classpath directives
+        final ResourcesDesc rdesc = new ResourcesDesc();
+
+        // Iterate over all entries
+        XMLUtils.visitElements(root, "<resources>",
+                new XMLUtils.ElementVisitor() {
+            @Override
+            public void visitElement(XMLNode e)
+                    throws MissingFieldException, BadFieldException {
+                // Check for right os, archictecture, and locale
+                String[] os = GeneralUtil.getStringList(
+                        XMLUtils.getAttribute(e, "", "os", null));
+                final String arch = XMLUtils.getAttribute(e, "", "arch", null);
+                String[] locale = GeneralUtil.getStringList(
+                        XMLUtils.getAttribute(e, "", "locale", null));
+                if (GeneralUtil.prefixMatchStringList(
+                        os, GeneralUtil.getOSFullName())
+                        && matchDefaultLocale(locale)) {
+                    // Now visit all children in this node
+                    XMLUtils.visitChildrenElements(e,
+                            new XMLUtils.ElementVisitor() {
+                        @Override
+                        public void visitElement(XMLNode e2)
+                                throws MissingFieldException, BadFieldException {
+                            handleResourceElement(source, codebase,
+                                    e2, rdesc, ignoreJres, arch, jnlpd);
+                        }
+                    });
+                }
+            }
+        });
+
+        if (!rdesc.isEmpty()) {
+            jnlpd.setResourcesDesc(rdesc);
+        }
+    }
+
+    private static IconDesc[] getIconDescs(final String source,
+            final URL codebase, XMLNode e)
+            throws MissingFieldException, BadFieldException {
+        final ArrayList<IconDesc> answer = new ArrayList<>();
+        XMLUtils.visitElements(e, "<icon>", new XMLUtils.ElementVisitor() {
+            @Override
+            public void visitElement(XMLNode icon) throws
+                    MissingFieldException, BadFieldException {
+                String kindStr = XMLUtils.getAttribute(icon, "", "kind", "");
+                URL href = XMLUtils.getRequiredURL(source, codebase, icon, "", "href");
+
+                if (href != null) {
+                    if (!JNLPConverter.isIconSupported(href.toExternalForm())) {
+                        return;
+                    }
+                }
+
+                int kind;
+                if (kindStr == null || kindStr.isEmpty() || kindStr.equals("default")) {
+                    kind = IconDesc.ICON_KIND_DEFAULT;
+                } else if (kindStr.equals("shortcut")) {
+                    kind = IconDesc.ICON_KIND_SHORTCUT;
+                } else {
+                    Log.warning("Ignoring unsupported icon \"" + href + "\" with kind \"" + kindStr + "\".");
+                    return;
+                }
+
+                answer.add(new IconDesc(href, kind));
+            }
+        });
+        return answer.toArray(new IconDesc[answer.size()]);
+    }
+
+    private static ShortcutDesc getShortcutDesc(XMLNode e)
+                throws MissingFieldException, BadFieldException {
+        final ArrayList<ShortcutDesc> shortcuts = new ArrayList<>();
+
+        XMLUtils.visitElements(e, "<shortcut>", new XMLUtils.ElementVisitor() {
+            @Override
+            public void visitElement(XMLNode shortcutNode)
+                throws MissingFieldException, BadFieldException {
+                boolean desktopHinted =
+                    XMLUtils.isElementPath(shortcutNode, "<desktop>");
+                boolean menuHinted =
+                    XMLUtils.isElementPath(shortcutNode, "<menu>");
+                String submenuHinted =
+                    XMLUtils.getAttribute(shortcutNode, "<menu>", "submenu");
+                shortcuts.add(new ShortcutDesc(desktopHinted, menuHinted, submenuHinted));
+            }
+        });
+
+        if (shortcuts.size() > 0) {
+            return shortcuts.get(0);
+        }
+        return null;
+    }
+
+    private static AssociationDesc[] getAssociationDesc(final String source,
+        final URL codebase, XMLNode e)
+                throws MissingFieldException, BadFieldException {
+        final ArrayList<AssociationDesc> answer = new ArrayList<>();
+        XMLUtils.visitElements(e, "<association>",
+            new XMLUtils.ElementVisitor() {
+            @Override
+            public void visitElement(XMLNode node)
+                throws MissingFieldException, BadFieldException {
+
+                String extensions = XMLUtils.getAttribute(
+                                       node, "", "extensions");
+
+                String mimeType = XMLUtils.getAttribute(
+                                       node, "", "mime-type");
+                String description = XMLUtils.getElementContents(
+                                        node, "<description>");
+
+                URL icon = XMLUtils.getAttributeURL(
+                                source, codebase, node, "<icon>", "href");
+
+                if (!JNLPConverter.isIconSupported(icon.toExternalForm())) {
+                    icon = null;
+                }
+
+                if (extensions == null && mimeType == null) {
+                    throw new MissingFieldException(source,
+                                 "<association>(<extensions><mime-type>)");
+                } else if (extensions == null) {
+                    throw new MissingFieldException(source,
+                                     "<association><extensions>");
+                } else if (mimeType == null) {
+                    throw new MissingFieldException(source,
+                                     "<association><mime-type>");
+                }
+
+                // don't support uppercase extension and mime-type on gnome.
+                if ("gnome".equals(System.getProperty("sun.desktop"))) {
+                    extensions = extensions.toLowerCase();
+                    mimeType = mimeType.toLowerCase();
+                }
+
+                answer.add(new AssociationDesc(extensions, mimeType,
+                                                description, icon));
+            }
+        });
+        return answer.toArray(
+                new AssociationDesc[answer.size()]);
+    }
+
+    /** Handle the individual entries in a resource desc */
+    private static void handleResourceElement(String source, URL codebase,
+        XMLNode e, ResourcesDesc rdesc, boolean ignoreJres, String arch, JNLPDesc jnlpd)
+        throws MissingFieldException, BadFieldException {
+
+        String tag = e.getName();
+
+        boolean matchArch = GeneralUtil.prefixMatchArch(
+            GeneralUtil.getStringList(arch));
+
+
+        if (matchArch && (tag.equals("jar") || tag.equals("nativelib"))) {
+            /*
+             * jar/nativelib elements
+             */
+            URL href = XMLUtils.getRequiredURL(source, codebase, e, "", "href");
+            String version = XMLUtils.getAttribute(e, "", "version", null);
+
+            String mainStr = XMLUtils.getAttribute(e, "", "main");
+            boolean isNativeLib = tag.equals("nativelib");
+
+            boolean isMain = "true".equalsIgnoreCase(mainStr);
+
+            JARDesc jd = new JARDesc(href, version, isMain, isNativeLib, rdesc);
+            rdesc.addResource(jd);
+        } else if (matchArch && tag.equals("property")) {
+            /*
+             *  property tag
+             */
+            String name  = XMLUtils.getRequiredAttribute(source, e, "", "name");
+            String value = XMLUtils.getRequiredAttributeEmptyOK(
+                    source, e, "", "value");
+
+            rdesc.addResource(new PropertyDesc(name, value));
+        } else if (matchArch && tag.equals("extension")) {
+            URL href = XMLUtils.getRequiredURL(source, codebase, e, "", "href");
+            String version = XMLUtils.getAttribute(e, "", "version", null);
+            rdesc.addResource(new ExtensionDesc(href, version));
+        } else if ((tag.equals("java") || tag.equals("j2se")) && !ignoreJres) {
+            /*
+             * j2se element
+             */
+            String version  =
+                XMLUtils.getRequiredAttribute(source, e, "", "version");
+            String minheapstr =
+                XMLUtils.getAttribute(e, "", "initial-heap-size");
+            String maxheapstr =
+                XMLUtils.getAttribute(e, "", "max-heap-size");
+
+            String vmargs =
+                XMLUtils.getAttribute(e, "", "java-vm-args");
+
+            if (jnlpd.isJRESet()) {
+                if (vmargs == null) {
+                    vmargs = "none";
+                }
+                Log.warning("Ignoring repeated element <" + tag + "> with version " + version +
+                        " and java-vm-args: " + vmargs);
+                return;
+            }
+
+            long minheap = GeneralUtil.heapValToLong(minheapstr);
+            long maxheap = GeneralUtil.heapValToLong(maxheapstr);
+
+            ResourcesDesc cbs = null;
+            buildResourcesDesc(source, codebase, e, true, null);
+
+            // JRE
+            JREDesc jreDesc = new JREDesc(
+                version,
+                minheap,
+                maxheap,
+                vmargs,
+                cbs,
+                arch);
+
+            rdesc.addResource(jreDesc);
+
+            jnlpd.setIsJRESet(true);
+        }
+    }
+
+    /** Extract data from the application-desc tag */
+    private static void buildApplicationDesc(final String source,
+        XMLNode root, JNLPDesc jnlpd) throws MissingFieldException, BadFieldException {
+
+        String mainclass = XMLUtils.getClassName(source, root,
+                           "<application-desc>", "main-class", false);
+        String appType = XMLUtils.getAttribute(root, "<application-desc>",
+                                               "type", "Java");
+        String progressclass  = XMLUtils.getClassName(source, root,
+                                "<application-desc>", "progress-class", false);
+        if (progressclass != null && !progressclass.isEmpty()) {
+            Log.warning("JNLPConverter does not support progress indication. \"" + progressclass + "\" will not be loaded and will be ignored.");
+        }
+
+        if (!("Java".equalsIgnoreCase(appType) ||
+            "JavaFx".equalsIgnoreCase(appType))) {
+            throw new BadFieldException(source, XMLUtils.getPathString(root) +
+                "<application-desc>type", appType);
+        }
+
+        if ("JavaFx".equalsIgnoreCase(appType)) {
+            jnlpd.setIsFXApp(true);
+        }
+
+        XMLUtils.visitElements(root, "<application-desc><argument>", new XMLUtils.ElementVisitor() {
+            @Override
+            public void visitElement(XMLNode e) throws MissingFieldException, BadFieldException {
+                String arg = XMLUtils.getElementContents(e, "", null);
+                if (arg == null) {
+                    throw new BadFieldException(source, XMLUtils.getPathString(e), "");
+                }
+                jnlpd.addArguments(arg);
+            }
+        });
+
+        XMLUtils.visitElements(root, "<application-desc><param>",
+            new XMLUtils.ElementVisitor() {
+            @Override
+            public void visitElement(XMLNode e) throws MissingFieldException,
+                BadFieldException {
+                String pn = XMLUtils.getRequiredAttribute(
+                            source, e, "", "name");
+                String pv = XMLUtils.getRequiredAttributeEmptyOK(
+                            source, e, "", "value");
+                jnlpd.setProperty(pn, pv);
+            }
+        });
+        jnlpd.setMainClass(mainclass, false);
+    }
+
+    /** Extract data from the javafx-desc tag */
+    private static void buildFXAppDesc(final String source,
+        XMLNode root, String element, JNLPDesc jnlpd)
+             throws MissingFieldException, BadFieldException {
+        String mainclass = XMLUtils.getClassName(source, root, element,
+                                                 "main-class", true);
+        String name = XMLUtils.getRequiredAttribute(source, root,
+                                        "<javafx-desc>", "name");
+
+        /* extract arguments */
+        XMLUtils.visitElements(root, "<javafx-desc><argument>", new XMLUtils.ElementVisitor() {
+            @Override
+            public void visitElement(XMLNode e) throws MissingFieldException, BadFieldException {
+                String arg = XMLUtils.getElementContents(e, "", null);
+                if (arg == null) {
+                    throw new BadFieldException(source, XMLUtils.getPathString(e), "");
+                }
+                jnlpd.addArguments(arg);
+            }
+        });
+
+        /* extract parameters */
+        XMLUtils.visitElements(root, "<javafx-desc><param>",
+            new XMLUtils.ElementVisitor() {
+            @Override
+            public void visitElement(XMLNode e) throws MissingFieldException,
+                BadFieldException {
+                String pn = XMLUtils.getRequiredAttribute(
+                            source, e, "", "name");
+                String pv = XMLUtils.getRequiredAttributeEmptyOK(
+                            source, e, "", "value");
+                jnlpd.setProperty(pn, pv);
+            }
+        });
+
+        jnlpd.setMainClass(mainclass, true);
+        jnlpd.setName(name);
+    }
+}