8233215: jpackage doesn't allow enough flexibility for file type binding

Reviewed-by: herrick, asemenyuk
This commit is contained in:
Alexander Matveev 2020-06-10 09:44:56 -04:00
parent d36a55d2ac
commit 191fe75c0f
5 changed files with 356 additions and 124 deletions

View File

@ -144,6 +144,80 @@ public class MacAppImageBuilder extends AbstractAppImageBuilder {
null : Boolean.valueOf(s)
);
private static final StandardBundlerParam<String> FA_MAC_CFBUNDLETYPEROLE =
new StandardBundlerParam<>(
Arguments.MAC_CFBUNDLETYPEROLE,
String.class,
params -> "Editor",
(s, p) -> s
);
private static final StandardBundlerParam<String> FA_MAC_LSHANDLERRANK =
new StandardBundlerParam<>(
Arguments.MAC_LSHANDLERRANK,
String.class,
params -> "Owner",
(s, p) -> s
);
private static final StandardBundlerParam<String> FA_MAC_NSSTORETYPEKEY =
new StandardBundlerParam<>(
Arguments.MAC_NSSTORETYPEKEY,
String.class,
params -> null,
(s, p) -> s
);
private static final StandardBundlerParam<String> FA_MAC_NSDOCUMENTCLASS =
new StandardBundlerParam<>(
Arguments.MAC_NSDOCUMENTCLASS,
String.class,
params -> null,
(s, p) -> s
);
private static final StandardBundlerParam<String> FA_MAC_LSTYPEISPACKAGE =
new StandardBundlerParam<>(
Arguments.MAC_LSTYPEISPACKAGE,
String.class,
params -> null,
(s, p) -> s
);
private static final StandardBundlerParam<String> FA_MAC_LSDOCINPLACE =
new StandardBundlerParam<>(
Arguments.MAC_LSDOCINPLACE,
String.class,
params -> null,
(s, p) -> s
);
private static final StandardBundlerParam<String> FA_MAC_UIDOCBROWSER =
new StandardBundlerParam<>(
Arguments.MAC_UIDOCBROWSER,
String.class,
params -> null,
(s, p) -> s
);
@SuppressWarnings("unchecked")
private static final StandardBundlerParam<List<String>> FA_MAC_NSEXPORTABLETYPES =
new StandardBundlerParam<>(
Arguments.MAC_NSEXPORTABLETYPES,
(Class<List<String>>) (Object) List.class,
params -> null,
(s, p) -> Arrays.asList(s.split("(,|\\s)+"))
);
@SuppressWarnings("unchecked")
private static final StandardBundlerParam<List<String>> FA_MAC_UTTYPECONFORMSTO =
new StandardBundlerParam<>(
Arguments.MAC_UTTYPECONFORMSTO,
(Class<List<String>>) (Object) List.class,
params -> Arrays.asList("public.data"),
(s, p) -> Arrays.asList(s.split("(,|\\s)+"))
);
public MacAppImageBuilder(Path imageOutDir) {
super(imageOutDir);
@ -314,6 +388,31 @@ public class MacAppImageBuilder extends AbstractAppImageBuilder {
.saveToFile(file);
}
private void writeStringArrayPlist(StringBuilder sb, String key,
List<String> values) {
if (values != null && !values.isEmpty()) {
sb.append(" <key>").append(key).append("</key>\n").append(" <array>\n");
values.forEach((value) -> {
sb.append(" <string>").append(value).append("</string>\n");
});
sb.append(" </array>\n");
}
}
private void writeStringPlist(StringBuilder sb, String key, String value) {
if (value != null && !value.isEmpty()) {
sb.append(" <key>").append(key).append("</key>\n").append(" <string>")
.append(value).append("</string>\n").append("\n");
}
}
private void writeBoolPlist(StringBuilder sb, String key, String value) {
if (value != null && !value.isEmpty()) {
sb.append(" <key>").append(key).append("</key>\n").append(" <")
.append(value).append("/>\n").append("\n");
}
}
private void writeInfoPlist(File file, Map<String, ? super Object> params)
throws IOException {
Log.verbose(MessageFormat.format(I18N.getString(
@ -338,113 +437,65 @@ public class MacAppImageBuilder extends AbstractAppImageBuilder {
fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
if (extensions == null) {
Log.verbose(I18N.getString(
"message.creating-association-with-null-extension"));
}
List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)
+ "." + ((extensions == null || extensions.isEmpty())
? "mime" : extensions.get(0));
String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
File icon = FA_ICON.fetchFrom(fileAssociation);
bundleDocumentTypes.append(" <dict>\n")
.append(" <key>LSItemContentTypes</key>\n")
.append(" <array>\n")
.append(" <string>")
.append(itemContentType)
.append("</string>\n")
.append(" </array>\n")
.append("\n")
.append(" <key>CFBundleTypeName</key>\n")
.append(" <string>")
.append(description)
.append("</string>\n")
.append("\n")
.append(" <key>LSHandlerRank</key>\n")
.append(" <string>Owner</string>\n")
// TODO make a bundler arg
.append("\n")
.append(" <key>CFBundleTypeRole</key>\n")
.append(" <string>Editor</string>\n")
// TODO make a bundler arg
.append("\n")
.append(" <key>LSIsAppleDefaultForType</key>\n")
.append(" <true/>\n")
// TODO make a bundler arg
.append("\n");
bundleDocumentTypes.append(" <dict>\n");
writeStringArrayPlist(bundleDocumentTypes, "LSItemContentTypes",
Arrays.asList(itemContentType));
writeStringPlist(bundleDocumentTypes, "CFBundleTypeName", description);
writeStringPlist(bundleDocumentTypes, "LSHandlerRank",
FA_MAC_LSHANDLERRANK.fetchFrom(fileAssociation));
writeStringPlist(bundleDocumentTypes, "CFBundleTypeRole",
FA_MAC_CFBUNDLETYPEROLE.fetchFrom(fileAssociation));
writeStringPlist(bundleDocumentTypes, "NSPersistentStoreTypeKey",
FA_MAC_NSSTORETYPEKEY.fetchFrom(fileAssociation));
writeStringPlist(bundleDocumentTypes, "NSDocumentClass",
FA_MAC_NSDOCUMENTCLASS.fetchFrom(fileAssociation));
writeBoolPlist(bundleDocumentTypes, "LSIsAppleDefaultForType",
"true");
writeBoolPlist(bundleDocumentTypes, "LSTypeIsPackage",
FA_MAC_LSTYPEISPACKAGE.fetchFrom(fileAssociation));
writeBoolPlist(bundleDocumentTypes, "LSSupportsOpeningDocumentsInPlace",
FA_MAC_LSDOCINPLACE.fetchFrom(fileAssociation));
writeBoolPlist(bundleDocumentTypes, "UISupportsDocumentBrowser",
FA_MAC_UIDOCBROWSER.fetchFrom(fileAssociation));
if (icon != null && icon.exists()) {
writeStringPlist(bundleDocumentTypes, "CFBundleTypeIconFile",
icon.getName());
}
bundleDocumentTypes.append(" </dict>\n");
exportedTypes.append(" <dict>\n");
writeStringPlist(exportedTypes, "UTTypeIdentifier",
itemContentType);
writeStringPlist(exportedTypes, "UTTypeDescription",
description);
writeStringArrayPlist(exportedTypes, "UTTypeConformsTo",
FA_MAC_UTTYPECONFORMSTO.fetchFrom(fileAssociation));
if (icon != null && icon.exists()) {
bundleDocumentTypes
.append(" <key>CFBundleTypeIconFile</key>\n")
.append(" <string>")
.append(icon.getName())
.append("</string>\n");
writeStringPlist(exportedTypes, "UTTypeIconFile", icon.getName());
}
bundleDocumentTypes.append(" </dict>\n");
exportedTypes.append(" <dict>\n")
.append(" <key>UTTypeIdentifier</key>\n")
.append(" <string>")
.append(itemContentType)
.append("</string>\n")
.append("\n")
.append(" <key>UTTypeDescription</key>\n")
.append(" <string>")
.append(description)
.append("</string>\n")
.append(" <key>UTTypeConformsTo</key>\n")
.append(" <array>\n")
.append(" <string>public.data</string>\n")
//TODO expose this?
.append(" </array>\n")
.append("\n");
if (icon != null && icon.exists()) {
exportedTypes.append(" <key>UTTypeIconFile</key>\n")
.append(" <string>")
.append(icon.getName())
.append("</string>\n")
.append("\n");
}
exportedTypes.append("\n")
.append(" <key>UTTypeTagSpecification</key>\n")
.append(" <dict>\n")
// TODO expose via param? .append(
// " <key>com.apple.ostype</key>\n");
// TODO expose via param? .append(
// " <string>ABCD</string>\n")
.append(" <key>UTTypeTagSpecification</key>\n")
.append(" <dict>\n")
.append("\n");
if (extensions != null && !extensions.isEmpty()) {
exportedTypes.append(
" <key>public.filename-extension</key>\n")
.append(" <array>\n");
for (String ext : extensions) {
exportedTypes.append(" <string>")
.append(ext)
.append("</string>\n");
}
exportedTypes.append(" </array>\n");
}
if (mimeTypes != null && !mimeTypes.isEmpty()) {
exportedTypes.append(" <key>public.mime-type</key>\n")
.append(" <array>\n");
for (String mime : mimeTypes) {
exportedTypes.append(" <string>")
.append(mime)
.append("</string>\n");
}
exportedTypes.append(" </array>\n");
}
exportedTypes.append(" </dict>\n")
.append(" </dict>\n");
writeStringArrayPlist(exportedTypes, "public.filename-extension",
extensions);
writeStringArrayPlist(exportedTypes, "public.mime-type",
FA_CONTENT_TYPE.fetchFrom(fileAssociation));
writeStringArrayPlist(exportedTypes, "NSExportableTypes",
FA_MAC_NSEXPORTABLETYPES.fetchFrom(fileAssociation));
exportedTypes.append(" </dict>\n").append(" </dict>\n");
}
String associationData;
if (bundleDocumentTypes.length() > 0) {

View File

@ -87,36 +87,36 @@ class AddLauncherArguments {
String module = getOptionValue(CLIOptions.MODULE);
if (module != null && mainClass != null) {
putUnlessNull(bundleParams, CLIOptions.MODULE.getId(),
Arguments.putUnlessNull(bundleParams, CLIOptions.MODULE.getId(),
module + "/" + mainClass);
} else if (module != null) {
putUnlessNull(bundleParams, CLIOptions.MODULE.getId(),
Arguments.putUnlessNull(bundleParams, CLIOptions.MODULE.getId(),
module);
} else {
putUnlessNull(bundleParams, CLIOptions.MAIN_JAR.getId(),
Arguments.putUnlessNull(bundleParams, CLIOptions.MAIN_JAR.getId(),
mainJar);
putUnlessNull(bundleParams, CLIOptions.APPCLASS.getId(),
Arguments.putUnlessNull(bundleParams, CLIOptions.APPCLASS.getId(),
mainClass);
}
putUnlessNull(bundleParams, CLIOptions.NAME.getId(),
Arguments.putUnlessNull(bundleParams, CLIOptions.NAME.getId(),
getOptionValue(CLIOptions.NAME));
putUnlessNull(bundleParams, CLIOptions.VERSION.getId(),
Arguments.putUnlessNull(bundleParams, CLIOptions.VERSION.getId(),
getOptionValue(CLIOptions.VERSION));
putUnlessNull(bundleParams, CLIOptions.RELEASE.getId(),
Arguments.putUnlessNull(bundleParams, CLIOptions.RELEASE.getId(),
getOptionValue(CLIOptions.RELEASE));
putUnlessNull(bundleParams, CLIOptions.LINUX_CATEGORY.getId(),
Arguments.putUnlessNull(bundleParams, CLIOptions.LINUX_CATEGORY.getId(),
getOptionValue(CLIOptions.LINUX_CATEGORY));
putUnlessNull(bundleParams,
Arguments.putUnlessNull(bundleParams,
CLIOptions.WIN_CONSOLE_HINT.getId(),
getOptionValue(CLIOptions.WIN_CONSOLE_HINT));
String value = getOptionValue(CLIOptions.ICON);
putUnlessNull(bundleParams, CLIOptions.ICON.getId(),
Arguments.putUnlessNull(bundleParams, CLIOptions.ICON.getId(),
(value == null) ? null : new File(value));
// "arguments" and "java-options" even if value is null:
@ -152,13 +152,6 @@ class AddLauncherArguments {
return bundleParams;
}
private void putUnlessNull(Map<String, ? super Object> params,
String param, Object value) {
if (value != null) {
params.put(param, value);
}
}
static Map<String, ? super Object> merge(
Map<String, ? super Object> original,
Map<String, ? super Object> additional, String... exclude) {

View File

@ -70,6 +70,20 @@ public class Arguments {
private static final String FA_DESCRIPTION = "description";
private static final String FA_ICON = "icon";
// Mac specific file association keys
// String
public static final String MAC_CFBUNDLETYPEROLE = "mac.CFBundleTypeRole";
public static final String MAC_LSHANDLERRANK = "mac.LSHandlerRank";
public static final String MAC_NSSTORETYPEKEY = "mac.NSPersistentStoreTypeKey";
public static final String MAC_NSDOCUMENTCLASS = "mac.NSDocumentClass";
// Boolean
public static final String MAC_LSTYPEISPACKAGE = "mac.LSTypeIsPackage";
public static final String MAC_LSDOCINPLACE = "mac.LSSupportsOpeningDocumentsInPlace";
public static final String MAC_UIDOCBROWSER = "mac.UISupportsDocumentBrowser";
// Array of strings
public static final String MAC_NSEXPORTABLETYPES = "mac.NSExportableTypes";
public static final String MAC_UTTYPECONFORMSTO = "mac.UTTypeConformsTo";
// regexp for parsing args (for example, for additional launchers)
private static Pattern pattern = Pattern.compile(
"(?:(?:([\"'])(?:\\\\\\1|.)*?(?:\\1|$))|(?:\\\\[\"'\\s]|[^\\s]))++");
@ -197,25 +211,45 @@ public class Arguments {
// load .properties file
Map<String, String> initialMap = getPropertiesFromFile(popArg());
String ext = initialMap.get(FA_EXTENSIONS);
if (ext != null) {
args.put(StandardBundlerParam.FA_EXTENSIONS.getID(), ext);
}
putUnlessNull(args, StandardBundlerParam.FA_EXTENSIONS.getID(),
initialMap.get(FA_EXTENSIONS));
String type = initialMap.get(FA_CONTENT_TYPE);
if (type != null) {
args.put(StandardBundlerParam.FA_CONTENT_TYPE.getID(), type);
}
putUnlessNull(args, StandardBundlerParam.FA_CONTENT_TYPE.getID(),
initialMap.get(FA_CONTENT_TYPE));
String desc = initialMap.get(FA_DESCRIPTION);
if (desc != null) {
args.put(StandardBundlerParam.FA_DESCRIPTION.getID(), desc);
}
putUnlessNull(args, StandardBundlerParam.FA_DESCRIPTION.getID(),
initialMap.get(FA_DESCRIPTION));
String icon = initialMap.get(FA_ICON);
if (icon != null) {
args.put(StandardBundlerParam.FA_ICON.getID(), icon);
}
putUnlessNull(args, StandardBundlerParam.FA_ICON.getID(),
initialMap.get(FA_ICON));
// Mac extended file association arguments
putUnlessNull(args, MAC_CFBUNDLETYPEROLE,
initialMap.get(MAC_CFBUNDLETYPEROLE));
putUnlessNull(args, MAC_LSHANDLERRANK,
initialMap.get(MAC_LSHANDLERRANK));
putUnlessNull(args, MAC_NSSTORETYPEKEY,
initialMap.get(MAC_NSSTORETYPEKEY));
putUnlessNull(args, MAC_NSDOCUMENTCLASS,
initialMap.get(MAC_NSDOCUMENTCLASS));
putUnlessNull(args, MAC_LSTYPEISPACKAGE,
initialMap.get(MAC_LSTYPEISPACKAGE));
putUnlessNull(args, MAC_LSDOCINPLACE,
initialMap.get(MAC_LSDOCINPLACE));
putUnlessNull(args, MAC_UIDOCBROWSER,
initialMap.get(MAC_UIDOCBROWSER));
putUnlessNull(args, MAC_NSEXPORTABLETYPES,
initialMap.get(MAC_NSEXPORTABLETYPES));
putUnlessNull(args, MAC_UTTYPECONFORMSTO,
initialMap.get(MAC_UTTYPECONFORMSTO));
ArrayList<Map<String, ? super Object>> associationList =
new ArrayList<Map<String, ? super Object>>();
@ -726,6 +760,13 @@ public class Arguments {
return list;
}
static void putUnlessNull(Map<String, ? super Object> params,
String param, Object value) {
if (value != null) {
params.put(param, value);
}
}
private static String unquoteIfNeeded(String in) {
if (in == null) {
return null;

View File

@ -28,6 +28,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -42,6 +43,7 @@ import jdk.jpackage.test.Functional.ThrowingConsumer;
import jdk.jpackage.test.Functional.ThrowingSupplier;
import jdk.jpackage.test.PackageTest.PackageHandlers;
import org.xml.sax.SAXException;
import org.w3c.dom.NodeList;
public class MacHelper {
@ -187,6 +189,40 @@ public class MacHelper {
query, doc, XPathConstants.STRING)).get();
}
public Boolean queryBoolValue(String keyName) {
XPath xPath = XPathFactory.newInstance().newXPath();
// Query boolean element preceding <key> element
// with value equal to `keyName`
String query = String.format(
"name(//*[preceding-sibling::key = \"%s\"])", keyName);
String value = ThrowingSupplier.toSupplier(() -> (String) xPath.evaluate(
query, doc, XPathConstants.STRING)).get();
return Boolean.valueOf(value);
}
public List<String> queryArrayValue(String keyName) {
XPath xPath = XPathFactory.newInstance().newXPath();
// Query string array preceding <key> element with value equal to `keyName`
String query = String.format(
"//array[preceding-sibling::key = \"%s\"]", keyName);
NodeList list = ThrowingSupplier.toSupplier(() -> (NodeList) xPath.evaluate(
query, doc, XPathConstants.NODESET)).get();
if (list.getLength() != 1) {
throw new RuntimeException(
String.format("Unable to find <array> element for key = \"%s\"]",
keyName));
}
NodeList childList = list.item(0).getChildNodes();
List<String> values = new ArrayList(childList.getLength());
for (int i = 0; i < childList.getLength(); i++) {
if (childList.item(i).getNodeName().equals("string")) {
values.add(childList.item(i).getTextContent());
}
}
return values;
}
PListWrapper(String xml) throws ParserConfigurationException,
SAXException, IOException {
doc = createDocumentBuilder().parse(new ByteArrayInputStream(

View File

@ -0,0 +1,111 @@
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static java.util.Map.entry;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.TKit;
import jdk.jpackage.test.MacHelper;
import jdk.jpackage.test.MacHelper.PListWrapper;
/**
* Tests generation of app image with --file-associations and mac additional file
* association arguments. Test will verify that arguments correctly propagated to
* Info.plist.
*/
/*
* @test
* @summary jpackage with --file-associations and mac specific file association args
* @library ../helpers
* @build jdk.jpackage.test.*
* @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal
* @requires (os.family == "mac")
* @run main/othervm -Xmx512m MacFileAssociationsTest
*/
public class MacFileAssociationsTest {
public static void main(String[] args) throws Exception {
TKit.run(args, () -> {
final Path propFile = TKit.workDir().resolve("fa.properties");
Map<String,String> map = Map.ofEntries(
entry("mime-type", "application/x-jpackage-foo"),
entry("extension", "foo"),
entry("description", "bar"),
entry("mac.CFBundleTypeRole", "Viewer"),
entry("mac.LSHandlerRank", "Default"),
entry("mac.NSDocumentClass", "SomeClass"),
entry("mac.LSTypeIsPackage", "true"),
entry("mac.LSSupportsOpeningDocumentsInPlace", "false"),
entry("mac.UISupportsDocumentBrowser", "false"),
entry("mac.NSExportableTypes", "public.png, public.jpg"),
entry("mac.UTTypeConformsTo", "public.image, public.data"));
TKit.createPropertiesFile(propFile, map);
JPackageCommand cmd = JPackageCommand.helloAppImage();
cmd.addArguments("--file-associations", propFile);
cmd.executeAndAssertHelloAppImageCreated();
Path appImage = cmd.outputBundle();
verifyPList(appImage);
});
}
private static void checkStringValue(PListWrapper plist, String key, String value) {
String result = plist.queryValue(key);
TKit.assertEquals(value, result, String.format(
"Check value of %s plist key", key));
}
private static void checkBoolValue(PListWrapper plist, String key, Boolean value) {
Boolean result = plist.queryBoolValue(key);
TKit.assertEquals(value.toString(), result.toString(), String.format(
"Check value of %s plist key", key));
}
private static void checkArrayValue(PListWrapper plist, String key,
List<String> values) {
List<String> result = plist.queryArrayValue(key);
TKit.assertStringListEquals(values, result, String.format(
"Check value of %s plist key", key));
}
private static void verifyPList(Path appImage) throws Exception {
PListWrapper plist = MacHelper.readPListFromAppImage(appImage);
checkStringValue(plist, "CFBundleTypeRole", "Viewer");
checkStringValue(plist, "LSHandlerRank", "Default");
checkStringValue(plist, "NSDocumentClass", "SomeClass");
checkBoolValue(plist, "LSTypeIsPackage", true);
checkBoolValue(plist, "LSSupportsOpeningDocumentsInPlace", false);
checkBoolValue(plist, "UISupportsDocumentBrowser", false);
checkArrayValue(plist, "NSExportableTypes", List.of("public.png",
"public.jpg"));
checkArrayValue(plist, "UTTypeConformsTo", List.of("public.image",
"public.data"));
}
}