8198472: Add support for creating bundles from JNLP files
Submitten-by: almatvee
Reviewed-by: herrick, kcr, prr, asemenyuk
/*
* 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);
}
}