# HG changeset patch # User herrick # Date 1571234243 14400 # Node ID 2c43b89b1679c8986b2fe62b3fd415ee0f77bc6e # Parent a561014c28d051e2fe52b3e1435dfd162ce0c406 8231862: Decouple DesktopIntegration and LinuxPackageBundler classes Submitted-by: asemenyuk Reviewed-by: aherrick, almatvee diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java Wed Oct 16 09:57:23 2019 -0400 @@ -0,0 +1,478 @@ +/* + * 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.awt.image.BufferedImage; +import java.io.*; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.imageio.ImageIO; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import static jdk.jpackage.internal.LinuxAppBundler.ICON_PNG; +import static jdk.jpackage.internal.LinuxAppImageBuilder.DEFAULT_ICON; +import static jdk.jpackage.internal.OverridableResource.createResource; +import static jdk.jpackage.internal.StandardBundlerParam.*; + +/** + * Helper to create files for desktop integration. + */ +final class DesktopIntegration { + + static final String DESKTOP_COMMANDS_INSTALL = "DESKTOP_COMMANDS_INSTALL"; + static final String DESKTOP_COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL"; + static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS"; + + DesktopIntegration(PlatformPackage thePackage, + Map params) { + + associations = FILE_ASSOCIATIONS.fetchFrom(params).stream().filter( + a -> { + if (a == null) { + return false; + } + List mimes = FA_CONTENT_TYPE.fetchFrom(a); + return (mimes != null && !mimes.isEmpty()); + }).collect(Collectors.toUnmodifiableList()); + + launchers = ADD_LAUNCHERS.fetchFrom(params); + + this.thePackage = thePackage; + + final File customIconFile = ICON_PNG.fetchFrom(params); + + iconResource = createResource(DEFAULT_ICON, params) + .setCategory(I18N.getString("resource.menu-icon")) + .setExternal(customIconFile); + desktopFileResource = createResource("template.desktop", params) + .setCategory(I18N.getString("resource.menu-shortcut-descriptor")); + + // XDG recommends to use vendor prefix in desktop file names as xdg + // commands copy files to system directories. + // Package name should be a good prefix. + final String desktopFileName = String.format("%s-%s.desktop", + thePackage.name(), APP_NAME.fetchFrom(params)); + final String mimeInfoFileName = String.format("%s-%s-MimeInfo.xml", + thePackage.name(), APP_NAME.fetchFrom(params)); + + mimeInfoFile = new DesktopFile(mimeInfoFileName); + + if (!associations.isEmpty() || SHORTCUT_HINT.fetchFrom(params) || customIconFile != null) { + // + // Create primary .desktop file if one of conditions is met: + // - there are file associations configured + // - user explicitely requested to create a shortcut + // - custom icon specified + // + desktopFile = new DesktopFile(desktopFileName); + iconFile = new DesktopFile(String.format("%s.png", + APP_NAME.fetchFrom(params))); + } else { + desktopFile = null; + iconFile = null; + } + + desktopFileData = Collections.unmodifiableMap( + createDataForDesktopFile(params)); + + nestedIntegrations = launchers.stream().map( + launcherParams -> new DesktopIntegration(thePackage, + launcherParams)).collect(Collectors.toList()); + } + + List requiredPackages() { + return Stream.of(List.of(this), nestedIntegrations).flatMap( + List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap( + List::stream).distinct().collect(Collectors.toList()); + } + + Map create() throws IOException { + if (iconFile != null) { + // Create application icon file. + iconResource.saveToFile(iconFile.srcPath()); + } + + Map data = new HashMap<>(desktopFileData); + + final ShellCommands shellCommands; + if (desktopFile != null) { + // Create application desktop description file. + createDesktopFile(data); + + // Shell commands will be created only if desktop file + // should be installed. + shellCommands = new ShellCommands(); + } else { + shellCommands = null; + } + + if (!associations.isEmpty()) { + // Create XML file with mime types corresponding to file associations. + createFileAssociationsMimeInfoFile(); + + shellCommands.setFileAssociations(); + + // Create icon files corresponding to file associations + Map mimeTypeWithIconFile = createFileAssociationIconFiles(); + mimeTypeWithIconFile.forEach((k, v) -> { + shellCommands.addIcon(k, v); + }); + } + + // Create shell commands to install/uninstall integration with desktop of the app. + if (shellCommands != null) { + shellCommands.applyTo(data); + } + + boolean needCleanupScripts = !associations.isEmpty(); + + // Take care of additional launchers if there are any. + // Process every additional launcher as the main application launcher. + // Collect shell commands to install/uninstall integration with desktop + // of the additional launchers and append them to the corresponding + // commands of the main launcher. + List installShellCmds = new ArrayList<>(Arrays.asList( + data.get(DESKTOP_COMMANDS_INSTALL))); + List uninstallShellCmds = new ArrayList<>(Arrays.asList( + data.get(DESKTOP_COMMANDS_UNINSTALL))); + for (var integration: nestedIntegrations) { + if (!integration.associations.isEmpty()) { + needCleanupScripts = true; + } + + Map launcherData = integration.create(); + + installShellCmds.add(launcherData.get(DESKTOP_COMMANDS_INSTALL)); + uninstallShellCmds.add(launcherData.get( + DESKTOP_COMMANDS_UNINSTALL)); + } + + data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands( + installShellCmds)); + data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands( + uninstallShellCmds)); + + if (needCleanupScripts) { + // Pull in utils.sh scrips library. + try (InputStream is = OverridableResource.readDefault("utils.sh"); + InputStreamReader isr = new InputStreamReader(is); + BufferedReader reader = new BufferedReader(isr)) { + data.put(UTILITY_SCRIPTS, reader.lines().collect( + Collectors.joining(System.lineSeparator()))); + } + } else { + data.put(UTILITY_SCRIPTS, ""); + } + + return data; + } + + private List requiredPackagesSelf() { + if (desktopFile != null) { + return List.of("xdg-utils"); + } + return Collections.emptyList(); + } + + private Map createDataForDesktopFile( + Map params) { + Map data = new HashMap<>(); + data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params)); + data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params)); + data.put("APPLICATION_ICON", + iconFile != null ? iconFile.installPath().toString() : null); + data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params)); + data.put("APPLICATION_LAUNCHER", + thePackage.installedApplicationLayout().launchersDirectory().resolve( + LinuxAppImageBuilder.getLauncherName(params)).toString()); + + return data; + } + + /** + * Shell commands to integrate something with desktop. + */ + private class ShellCommands { + + ShellCommands() { + registerIconCmds = new ArrayList<>(); + unregisterIconCmds = new ArrayList<>(); + + registerDesktopFileCmd = String.join(" ", "xdg-desktop-menu", + "install", desktopFile.installPath().toString()); + unregisterDesktopFileCmd = String.join(" ", "xdg-desktop-menu", + "uninstall", desktopFile.installPath().toString()); + } + + void setFileAssociations() { + registerFileAssociationsCmd = String.join(" ", "xdg-mime", + "install", + mimeInfoFile.installPath().toString()); + unregisterFileAssociationsCmd = String.join(" ", "xdg-mime", + "uninstall", mimeInfoFile.installPath().toString()); + + // + // Add manual cleanup of system files to get rid of + // the default mime type handlers. + // + // Even after mime type is unregisterd with `xdg-mime uninstall` + // command and desktop file deleted with `xdg-desktop-menu uninstall` + // command, records in + // `/usr/share/applications/defaults.list` (Ubuntu 16) or + // `/usr/local/share/applications/defaults.list` (OracleLinux 7) + // files remain referencing deleted mime time and deleted + // desktop file which makes `xdg-mime query default` output name + // of non-existing desktop file. + // + String cleanUpCommand = String.join(" ", + "uninstall_default_mime_handler", + desktopFile.installPath().getFileName().toString(), + String.join(" ", getMimeTypeNamesFromFileAssociations())); + + unregisterFileAssociationsCmd = stringifyShellCommands( + unregisterFileAssociationsCmd, cleanUpCommand); + } + + void addIcon(String mimeType, Path iconFile) { + final int imgSize = getSquareSizeOfImage(iconFile.toFile()); + final String dashMime = mimeType.replace('/', '-'); + registerIconCmds.add(String.join(" ", "xdg-icon-resource", + "install", "--context", "mimetypes", "--size ", + Integer.toString(imgSize), iconFile.toString(), dashMime)); + unregisterIconCmds.add(String.join(" ", "xdg-icon-resource", + "uninstall", dashMime)); + } + + void applyTo(Map data) { + List cmds = new ArrayList<>(); + + cmds.add(registerDesktopFileCmd); + cmds.add(registerFileAssociationsCmd); + cmds.addAll(registerIconCmds); + data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands(cmds)); + + cmds.clear(); + cmds.add(unregisterDesktopFileCmd); + cmds.add(unregisterFileAssociationsCmd); + cmds.addAll(unregisterIconCmds); + data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands(cmds)); + } + + private String registerDesktopFileCmd; + private String unregisterDesktopFileCmd; + + private String registerFileAssociationsCmd; + private String unregisterFileAssociationsCmd; + + private List registerIconCmds; + private List unregisterIconCmds; + } + + /** + * Desktop integration file. xml, icon, etc. + * Resides somewhere in application installation tree. + * Has two paths: + * - path where it should be placed at package build time; + * - path where it should be installed by package manager; + */ + private class DesktopFile { + + DesktopFile(String fileName) { + installPath = thePackage + .installedApplicationLayout() + .destktopIntegrationDirectory().resolve(fileName); + srcPath = thePackage + .sourceApplicationLayout() + .destktopIntegrationDirectory().resolve(fileName); + } + + private final Path installPath; + private final Path srcPath; + + Path installPath() { + return installPath; + } + + Path srcPath() { + return srcPath; + } + } + + private void appendFileAssociation(XMLStreamWriter xml, + Map assoc) throws XMLStreamException { + + xml.writeStartElement("mime-type"); + final String thisMime = FA_CONTENT_TYPE.fetchFrom(assoc).get(0); + xml.writeAttribute("type", thisMime); + + final String description = FA_DESCRIPTION.fetchFrom(assoc); + if (description != null && !description.isEmpty()) { + xml.writeStartElement("comment"); + xml.writeCharacters(description); + xml.writeEndElement(); + } + + final List extensions = FA_EXTENSIONS.fetchFrom(assoc); + if (extensions == null) { + Log.error(I18N.getString( + "message.creating-association-with-null-extension")); + } else { + for (String ext : extensions) { + xml.writeStartElement("glob"); + xml.writeAttribute("pattern", "*." + ext); + xml.writeEndElement(); + } + } + + xml.writeEndElement(); + } + + private void createFileAssociationsMimeInfoFile() throws IOException { + XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance(); + + try (Writer w = new BufferedWriter(new FileWriter( + mimeInfoFile.srcPath().toFile()))) { + XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w); + + xml.writeStartDocument(); + xml.writeStartElement("mime-info"); + xml.writeNamespace("xmlns", + "http://www.freedesktop.org/standards/shared-mime-info"); + + for (var assoc : associations) { + appendFileAssociation(xml, assoc); + } + + xml.writeEndElement(); + xml.writeEndDocument(); + xml.flush(); + xml.close(); + + } catch (XMLStreamException ex) { + Log.verbose(ex); + throw new IOException(ex); + } + } + + private Map createFileAssociationIconFiles() throws + IOException { + Map mimeTypeWithIconFile = new HashMap<>(); + for (var assoc : associations) { + File customFaIcon = FA_ICON.fetchFrom(assoc); + if (customFaIcon == null || !customFaIcon.exists() || getSquareSizeOfImage( + customFaIcon) == 0) { + continue; + } + + String fname = iconFile.srcPath().getFileName().toString(); + if (fname.indexOf(".") > 0) { + fname = fname.substring(0, fname.lastIndexOf(".")); + } + + DesktopFile faIconFile = new DesktopFile( + fname + "_fa_" + customFaIcon.getName()); + + IOUtils.copyFile(customFaIcon, faIconFile.srcPath().toFile()); + + mimeTypeWithIconFile.put(FA_CONTENT_TYPE.fetchFrom(assoc).get(0), + faIconFile.installPath()); + } + return mimeTypeWithIconFile; + } + + private void createDesktopFile(Map data) throws IOException { + List mimeTypes = getMimeTypeNamesFromFileAssociations(); + data.put("DESKTOP_MIMES", "MimeType=" + String.join(";", mimeTypes)); + + // prepare desktop shortcut + desktopFileResource + .setSubstitutionData(data) + .saveToFile(desktopFile.srcPath()); + } + + private List getMimeTypeNamesFromFileAssociations() { + return associations.stream().map( + a -> FA_CONTENT_TYPE.fetchFrom(a).get(0)).collect( + Collectors.toUnmodifiableList()); + } + + private static int getSquareSizeOfImage(File f) { + try { + BufferedImage bi = ImageIO.read(f); + if (bi.getWidth() == bi.getHeight()) { + return bi.getWidth(); + } + } catch (IOException e) { + Log.verbose(e); + } + return 0; + } + + private static String stringifyShellCommands(String... commands) { + return stringifyShellCommands(Arrays.asList(commands)); + } + + private static String stringifyShellCommands(List commands) { + return String.join(System.lineSeparator(), commands.stream().filter( + s -> s != null && !s.isEmpty()).collect(Collectors.toList())); + } + + private final PlatformPackage thePackage; + + private final List> associations; + + private final List> launchers; + + private final OverridableResource iconResource; + private final OverridableResource desktopFileResource; + + private final DesktopFile mimeInfoFile; + private final DesktopFile desktopFile; + private final DesktopFile iconFile; + + private final List nestedIntegrations; + + private final Map desktopFileData; + + private static final BundlerParamInfo MENU_GROUP = + new StandardBundlerParam<>( + Arguments.CLIOptions.LINUX_MENU_GROUP.getId(), + String.class, + params -> I18N.getString("param.menu-group.default"), + (s, p) -> s + ); + + private static final StandardBundlerParam SHORTCUT_HINT = + new StandardBundlerParam<>( + Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId(), + Boolean.class, + params -> false, + (s, p) -> (s == null || "null".equalsIgnoreCase(s)) + ? false : Boolean.valueOf(s) + ); +} diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java Wed Oct 16 09:57:23 2019 -0400 @@ -30,13 +30,11 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.ResourceBundle; +import static jdk.jpackage.internal.OverridableResource.createResource; import static jdk.jpackage.internal.StandardBundlerParam.*; @@ -170,19 +168,13 @@ private void copyIcon(Map params) throws IOException { - File icon = ICON_PNG.fetchFrom(params); - File iconTarget = appLayout.destktopIntegrationDirectory().resolve( - APP_NAME.fetchFrom(params) + ".png").toFile(); + Path iconTarget = appLayout.destktopIntegrationDirectory().resolve( + APP_NAME.fetchFrom(params) + ".png"); - InputStream in = locateResource( - iconTarget.getName(), - "icon", - DEFAULT_ICON, - icon, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - - Files.copy(in, iconTarget.toPath(), StandardCopyOption.REPLACE_EXISTING); + createResource(DEFAULT_ICON, params) + .setCategory("icon") + .setExternal(ICON_PNG.fetchFrom(params)) + .saveToFile(iconTarget); } private void copyApplication(Map params) diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java Wed Oct 16 09:57:23 2019 -0400 @@ -42,6 +42,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR; +import static jdk.jpackage.internal.OverridableResource.createResource; import static jdk.jpackage.internal.StandardBundlerParam.*; @@ -332,17 +333,11 @@ void create(Map data, Map params) throws IOException { - Files.createDirectories(dstFilePath.getParent()); - try (Writer w = Files.newBufferedWriter(dstFilePath)) { - String content = preprocessTextResource( - dstFilePath.getFileName().toString(), - I18N.getString(comment), - "template." + dstFilePath.getFileName().toString(), - data, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - w.write(content); - } + createResource("template." + dstFilePath.getFileName().toString(), + params) + .setCategory(I18N.getString(comment)) + .setSubstitutionData(data) + .saveToFile(dstFilePath); if (permissions != null) { setPermissions(dstFilePath.toFile(), permissions); } diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java Wed Oct 16 09:57:23 2019 -0400 @@ -24,65 +24,26 @@ */ package jdk.jpackage.internal; -import java.awt.image.BufferedImage; import java.io.*; import java.nio.file.InvalidPathException; -import java.nio.file.Files; import java.nio.file.Path; import java.text.MessageFormat; import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.imageio.ImageIO; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; +import static jdk.jpackage.internal.DesktopIntegration.*; import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR; import static jdk.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES; -import static jdk.jpackage.internal.LinuxAppImageBuilder.DEFAULT_ICON; -import static jdk.jpackage.internal.LinuxAppImageBuilder.ICON_PNG; import static jdk.jpackage.internal.StandardBundlerParam.*; abstract class LinuxPackageBundler extends AbstractBundler { - private static final String DESKTOP_COMMANDS_INSTALL = "DESKTOP_COMMANDS_INSTALL"; - private static final String DESKTOP_COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL"; - private static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS"; - - private static final BundlerParamInfo APP_BUNDLER = - new StandardBundlerParam<>( - "linux.app.bundler", - LinuxAppBundler.class, - (params) -> new LinuxAppBundler(), - null - ); - - private static final BundlerParamInfo MENU_GROUP = - new StandardBundlerParam<>( - Arguments.CLIOptions.LINUX_MENU_GROUP.getId(), - String.class, - params -> I18N.getString("param.menu-group.default"), - (s, p) -> s - ); - - private static final StandardBundlerParam SHORTCUT_HINT = - new StandardBundlerParam<>( - Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId(), - Boolean.class, - params -> false, - (s, p) -> (s == null || "null".equalsIgnoreCase(s)) - ? false : Boolean.valueOf(s) - ); - LinuxPackageBundler(BundlerParamInfo packageName) { this.packageName = packageName; } - private final BundlerParamInfo packageName; - private boolean withFindNeededPackages; - @Override final public boolean validate(Map params) throws ConfigException { @@ -361,449 +322,16 @@ } } - /** - * Helper to create files for desktop integration. - */ - private class DesktopIntegration { - - DesktopIntegration(PlatformPackage thePackage, - Map params) { - - associations = FILE_ASSOCIATIONS.fetchFrom(params).stream().filter( - a -> { - if (a == null) { - return false; - } - List mimes = FA_CONTENT_TYPE.fetchFrom(a); - return (mimes != null && !mimes.isEmpty()); - }).collect(Collectors.toUnmodifiableList()); - - launchers = ADD_LAUNCHERS.fetchFrom(params); - - this.thePackage = thePackage; - - customIconFile = ICON_PNG.fetchFrom(params); - - verbose = VERBOSE.fetchFrom(params); - resourceDir = RESOURCE_DIR.fetchFrom(params); - - // XDG recommends to use vendor prefix in desktop file names as xdg - // commands copy files to system directories. - // Package name should be a good prefix. - final String desktopFileName = String.format("%s-%s.desktop", - thePackage.name(), APP_NAME.fetchFrom(params)); - final String mimeInfoFileName = String.format("%s-%s-MimeInfo.xml", - thePackage.name(), APP_NAME.fetchFrom(params)); - - mimeInfoFile = new DesktopFile(mimeInfoFileName); - - if (!associations.isEmpty() || SHORTCUT_HINT.fetchFrom(params) || customIconFile != null) { - // - // Create primary .desktop file if one of conditions is met: - // - there are file associations configured - // - user explicitely requested to create a shortcut - // - custom icon specified - // - desktopFile = new DesktopFile(desktopFileName); - iconFile = new DesktopFile(String.format("%s.png", - APP_NAME.fetchFrom(params))); - } else { - desktopFile = null; - iconFile = null; - } - - desktopFileData = Collections.unmodifiableMap( - createDataForDesktopFile(params)); - - nestedIntegrations = launchers.stream().map( - launcherParams -> new DesktopIntegration(thePackage, - launcherParams)).collect(Collectors.toList()); - } - - List requiredPackages() { - return Stream.of(List.of(this), nestedIntegrations).flatMap( - List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap( - List::stream).distinct().collect(Collectors.toList()); - } - - Map create() throws IOException { - if (iconFile != null) { - // Create application icon file. - prepareSrcIconFile(); - } - - Map data = new HashMap<>(desktopFileData); - - final ShellCommands shellCommands; - if (desktopFile != null) { - // Create application desktop description file. - createDesktopFile(data); - - // Shell commands will be created only if desktop file - // should be installed. - shellCommands = new ShellCommands(); - } else { - shellCommands = null; - } - - if (!associations.isEmpty()) { - // Create XML file with mime types corresponding to file associations. - createFileAssociationsMimeInfoFile(); - - shellCommands.setFileAssociations(); - - // Create icon files corresponding to file associations - Map mimeTypeWithIconFile = createFileAssociationIconFiles(); - mimeTypeWithIconFile.forEach((k, v) -> { - shellCommands.addIcon(k, v); - }); - } - - // Create shell commands to install/uninstall integration with desktop of the app. - if (shellCommands != null) { - shellCommands.applyTo(data); - } - - boolean needCleanupScripts = !associations.isEmpty(); - - // Take care of additional launchers if there are any. - // Process every additional launcher as the main application launcher. - // Collect shell commands to install/uninstall integration with desktop - // of the additional launchers and append them to the corresponding - // commands of the main launcher. - List installShellCmds = new ArrayList<>(Arrays.asList( - data.get(DESKTOP_COMMANDS_INSTALL))); - List uninstallShellCmds = new ArrayList<>(Arrays.asList( - data.get(DESKTOP_COMMANDS_UNINSTALL))); - for (var integration: nestedIntegrations) { - if (!integration.associations.isEmpty()) { - needCleanupScripts = true; - } - - Map launcherData = integration.create(); - - installShellCmds.add(launcherData.get(DESKTOP_COMMANDS_INSTALL)); - uninstallShellCmds.add(launcherData.get( - DESKTOP_COMMANDS_UNINSTALL)); - } - - data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands( - installShellCmds)); - data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands( - uninstallShellCmds)); - - if (needCleanupScripts) { - // Pull in utils.sh scrips library. - try (InputStream is = getResourceAsStream("utils.sh"); - InputStreamReader isr = new InputStreamReader(is); - BufferedReader reader = new BufferedReader(isr)) { - data.put(UTILITY_SCRIPTS, reader.lines().collect( - Collectors.joining(System.lineSeparator()))); - } - } else { - data.put(UTILITY_SCRIPTS, ""); - } - - return data; - } - - private List requiredPackagesSelf() { - if (desktopFile != null) { - return List.of("xdg-utils"); - } - return Collections.emptyList(); - } - - private Map createDataForDesktopFile( - Map params) { - Map data = new HashMap<>(); - data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params)); - data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params)); - data.put("APPLICATION_ICON", - iconFile != null ? iconFile.installPath().toString() : null); - data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params)); - data.put("APPLICATION_LAUNCHER", - thePackage.installedApplicationLayout().launchersDirectory().resolve( - LinuxAppImageBuilder.getLauncherName(params)).toString()); - - return data; - } - - /** - * Shell commands to integrate something with desktop. - */ - private class ShellCommands { - - ShellCommands() { - registerIconCmds = new ArrayList<>(); - unregisterIconCmds = new ArrayList<>(); - - registerDesktopFileCmd = String.join(" ", "xdg-desktop-menu", - "install", desktopFile.installPath().toString()); - unregisterDesktopFileCmd = String.join(" ", "xdg-desktop-menu", - "uninstall", desktopFile.installPath().toString()); - } - - void setFileAssociations() { - registerFileAssociationsCmd = String.join(" ", "xdg-mime", - "install", - mimeInfoFile.installPath().toString()); - unregisterFileAssociationsCmd = String.join(" ", "xdg-mime", - "uninstall", mimeInfoFile.installPath().toString()); - - // - // Add manual cleanup of system files to get rid of - // the default mime type handlers. - // - // Even after mime type is unregisterd with `xdg-mime uninstall` - // command and desktop file deleted with `xdg-desktop-menu uninstall` - // command, records in - // `/usr/share/applications/defaults.list` (Ubuntu 16) or - // `/usr/local/share/applications/defaults.list` (OracleLinux 7) - // files remain referencing deleted mime time and deleted - // desktop file which makes `xdg-mime query default` output name - // of non-existing desktop file. - // - String cleanUpCommand = String.join(" ", - "uninstall_default_mime_handler", - desktopFile.installPath().getFileName().toString(), - String.join(" ", getMimeTypeNamesFromFileAssociations())); - - unregisterFileAssociationsCmd = stringifyShellCommands( - unregisterFileAssociationsCmd, cleanUpCommand); - } - - void addIcon(String mimeType, Path iconFile) { - final int imgSize = getSquareSizeOfImage(iconFile.toFile()); - final String dashMime = mimeType.replace('/', '-'); - registerIconCmds.add(String.join(" ", "xdg-icon-resource", - "install", "--context", "mimetypes", "--size ", - Integer.toString(imgSize), iconFile.toString(), dashMime)); - unregisterIconCmds.add(String.join(" ", "xdg-icon-resource", - "uninstall", dashMime)); - } + private final BundlerParamInfo packageName; + private boolean withFindNeededPackages; + private DesktopIntegration desktopIntegration; - void applyTo(Map data) { - List cmds = new ArrayList<>(); - - cmds.add(registerDesktopFileCmd); - cmds.add(registerFileAssociationsCmd); - cmds.addAll(registerIconCmds); - data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands(cmds)); - - cmds.clear(); - cmds.add(unregisterDesktopFileCmd); - cmds.add(unregisterFileAssociationsCmd); - cmds.addAll(unregisterIconCmds); - data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands(cmds)); - } - - private String registerDesktopFileCmd; - private String unregisterDesktopFileCmd; - - private String registerFileAssociationsCmd; - private String unregisterFileAssociationsCmd; - - private List registerIconCmds; - private List unregisterIconCmds; - } - - private final PlatformPackage thePackage; - - private final List> associations; - - private final List> launchers; - - /** - * Desktop integration file. xml, icon, etc. - * Resides somewhere in application installation tree. - * Has two paths: - * - path where it should be placed at package build time; - * - path where it should be installed by package manager; - */ - private class DesktopFile { - - DesktopFile(String fileName) { - installPath = thePackage - .installedApplicationLayout() - .destktopIntegrationDirectory().resolve(fileName); - srcPath = thePackage - .sourceApplicationLayout() - .destktopIntegrationDirectory().resolve(fileName); - } - - private final Path installPath; - private final Path srcPath; - - Path installPath() { - return installPath; - } - - Path srcPath() { - return srcPath; - } - } - - private final boolean verbose; - private final File resourceDir; - - private final DesktopFile mimeInfoFile; - private final DesktopFile desktopFile; - private final DesktopFile iconFile; - - final private List nestedIntegrations; - - private final Map desktopFileData; - - /** - * Path to icon file provided by user or null. - */ - private final File customIconFile; - - private void appendFileAssociation(XMLStreamWriter xml, - Map assoc) throws XMLStreamException { - - xml.writeStartElement("mime-type"); - final String thisMime = FA_CONTENT_TYPE.fetchFrom(assoc).get(0); - xml.writeAttribute("type", thisMime); - - final String description = FA_DESCRIPTION.fetchFrom(assoc); - if (description != null && !description.isEmpty()) { - xml.writeStartElement("comment"); - xml.writeCharacters(description); - xml.writeEndElement(); - } - - final List extensions = FA_EXTENSIONS.fetchFrom(assoc); - if (extensions == null) { - Log.error(I18N.getString( - "message.creating-association-with-null-extension")); - } else { - for (String ext : extensions) { - xml.writeStartElement("glob"); - xml.writeAttribute("pattern", "*." + ext); - xml.writeEndElement(); - } - } - - xml.writeEndElement(); - } - - private void createFileAssociationsMimeInfoFile() throws IOException { - XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance(); + private static final BundlerParamInfo APP_BUNDLER = + new StandardBundlerParam<>( + "linux.app.bundler", + LinuxAppBundler.class, + (params) -> new LinuxAppBundler(), + null + ); - try (Writer w = new BufferedWriter(new FileWriter( - mimeInfoFile.srcPath().toFile()))) { - XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w); - - xml.writeStartDocument(); - xml.writeStartElement("mime-info"); - xml.writeNamespace("xmlns", - "https://www.freedesktop.org/standards/shared-mime-info"); - - for (var assoc : associations) { - appendFileAssociation(xml, assoc); - } - - xml.writeEndElement(); - xml.writeEndDocument(); - xml.flush(); - xml.close(); - - } catch (XMLStreamException ex) { - Log.verbose(ex); - throw new IOException(ex); - } - } - - private Map createFileAssociationIconFiles() throws - IOException { - Map mimeTypeWithIconFile = new HashMap<>(); - for (var assoc : associations) { - File customFaIcon = FA_ICON.fetchFrom(assoc); - if (customFaIcon == null || !customFaIcon.exists() || getSquareSizeOfImage( - customFaIcon) == 0) { - continue; - } - - String fname = iconFile.srcPath().getFileName().toString(); - if (fname.indexOf(".") > 0) { - fname = fname.substring(0, fname.lastIndexOf(".")); - } - - DesktopFile faIconFile = new DesktopFile( - fname + "_fa_" + customFaIcon.getName()); - - IOUtils.copyFile(customFaIcon, faIconFile.srcPath().toFile()); - - mimeTypeWithIconFile.put(FA_CONTENT_TYPE.fetchFrom(assoc).get(0), - faIconFile.installPath()); - } - return mimeTypeWithIconFile; - } - - private void createDesktopFile(Map data) throws IOException { - List mimeTypes = getMimeTypeNamesFromFileAssociations(); - data.put("DESKTOP_MIMES", "MimeType=" + String.join(";", mimeTypes)); - - // prepare desktop shortcut - try (Writer w = Files.newBufferedWriter(desktopFile.srcPath())) { - String content = preprocessTextResource( - desktopFile.srcPath().getFileName().toString(), - I18N.getString("resource.menu-shortcut-descriptor"), - "template.desktop", - data, - verbose, - resourceDir); - w.write(content); - } - } - - private void prepareSrcIconFile() throws IOException { - if (customIconFile == null || !customIconFile.exists()) { - fetchResource(iconFile.srcPath().getFileName().toString(), - I18N.getString("resource.menu-icon"), - DEFAULT_ICON, - iconFile.srcPath().toFile(), - verbose, - resourceDir); - } else { - fetchResource(iconFile.srcPath().getFileName().toString(), - I18N.getString("resource.menu-icon"), - customIconFile, - iconFile.srcPath().toFile(), - verbose, - resourceDir); - } - } - - private List getMimeTypeNamesFromFileAssociations() { - return associations.stream().map( - a -> FA_CONTENT_TYPE.fetchFrom(a).get(0)).collect( - Collectors.toUnmodifiableList()); - } - } - - private static int getSquareSizeOfImage(File f) { - try { - BufferedImage bi = ImageIO.read(f); - if (bi.getWidth() == bi.getHeight()) { - return bi.getWidth(); - } - } catch (IOException e) { - Log.verbose(e); - } - return 0; - } - - private static String stringifyShellCommands(String ... commands) { - return stringifyShellCommands(Arrays.asList(commands)); - } - - private static String stringifyShellCommands(List commands) { - return String.join(System.lineSeparator(), commands.stream().filter( - s -> s != null && !s.isEmpty()).collect(Collectors.toList())); - } - - private DesktopIntegration desktopIntegration; } diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java Wed Oct 16 09:57:23 2019 -0400 @@ -26,7 +26,6 @@ package jdk.jpackage.internal; import java.io.*; -import java.nio.file.Files; import java.nio.file.Path; import java.text.MessageFormat; import java.util.*; @@ -36,6 +35,7 @@ import static jdk.jpackage.internal.StandardBundlerParam.*; import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR; +import static jdk.jpackage.internal.OverridableResource.createResource; /** * There are two command line options to configure license information for RPM @@ -146,16 +146,10 @@ Path specFile = specFile(params); // prepare spec file - Files.createDirectories(specFile.getParent()); - try (Writer w = Files.newBufferedWriter(specFile)) { - String content = preprocessTextResource( - specFile.getFileName().toString(), - I18N.getString("resource.rpm-spec-file"), - DEFAULT_SPEC_TEMPLATE, replacementData, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - w.write(content); - } + createResource(DEFAULT_SPEC_TEMPLATE, params) + .setCategory(I18N.getString("resource.rpm-spec-file")) + .setSubstitutionData(replacementData) + .saveToFile(specFile); return buildRPM(params, outputParentDir); } @@ -171,14 +165,11 @@ data.put("APPLICATION_LICENSE_TYPE", LICENSE_TYPE.fetchFrom(params)); String licenseFile = LICENSE_FILE.fetchFrom(params); - if (licenseFile == null) { - licenseFile = ""; - } else { + if (licenseFile != null) { licenseFile = Path.of(licenseFile).toAbsolutePath().normalize().toString(); } data.put("APPLICATION_LICENSE_FILE", licenseFile); - data.put("APPLICATION_GROUP", Optional.ofNullable( - GROUP.fetchFrom(params)).orElse("")); + data.put("APPLICATION_GROUP", GROUP.fetchFrom(params)); return data; } diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppBundler.java Wed Oct 16 09:57:23 2019 -0400 @@ -141,8 +141,7 @@ SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)); if (result != null) { - MacCertificate certificate = new MacCertificate(result, - VERBOSE.fetchFrom(params)); + MacCertificate certificate = new MacCertificate(result); if (!certificate.isValid()) { Log.error(MessageFormat.format(I18N.getString( diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppImageBuilder.java --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppImageBuilder.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppImageBuilder.java Wed Oct 16 09:57:23 2019 -0400 @@ -25,16 +25,10 @@ package jdk.jpackage.internal; -import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.UncheckedIOException; import java.io.Writer; import java.math.BigInteger; import java.nio.file.Files; @@ -63,6 +57,7 @@ import static jdk.jpackage.internal.StandardBundlerParam.*; import static jdk.jpackage.internal.MacBaseInstallerBundler.*; import static jdk.jpackage.internal.MacAppBundler.*; +import static jdk.jpackage.internal.OverridableResource.createResource; public class MacAppImageBuilder extends AbstractAppImageBuilder { @@ -143,13 +138,6 @@ }, (s, p) -> s); - public static final BundlerParamInfo DEFAULT_ICNS_ICON = - new StandardBundlerParam<>( - ".mac.default.icns", - String.class, - params -> TEMPLATE_BUNDLE_ICON, - (s, p) -> s); - public static final BundlerParamInfo ICON_ICNS = new StandardBundlerParam<>( "icon.icns", @@ -312,18 +300,12 @@ copyClassPathEntries(appDir, params); /*********** Take care of "config" files *******/ - File icon = ICON_ICNS.fetchFrom(params); - InputStream in = locateResource( - APP_NAME.fetchFrom(params) + ".icns", - "icon", - DEFAULT_ICNS_ICON.fetchFrom(params), - icon, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - Files.copy(in, - resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns"), - StandardCopyOption.REPLACE_EXISTING); + createResource(TEMPLATE_BUNDLE_ICON, params) + .setCategory("icon") + .setExternal(ICON_ICNS.fetchFrom(params)) + .saveToFile(resourcesDir.resolve(APP_NAME.fetchFrom(params) + + ".icns")); // copy file association icons for (Map params) @@ -664,16 +643,11 @@ } data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); - - try (Writer w = Files.newBufferedWriter(file.toPath())) { - w.write(preprocessTextResource( - // getConfig_InfoPlist(params).getName(), - "Info.plist", - I18N.getString("resource.app-info-plist"), - TEMPLATE_INFO_PLIST_LITE, - data, VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params))); - } + createResource(TEMPLATE_INFO_PLIST_LITE, params) + .setCategory(I18N.getString("resource.app-info-plist")) + .setSubstitutionData(data) + .setPublicName("Info.plist") + .saveToFile(file); } private void writePkgInfo(File file) throws IOException { diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppStoreBundler.java --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppStoreBundler.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppStoreBundler.java Wed Oct 16 09:57:23 2019 -0400 @@ -29,9 +29,6 @@ import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -39,6 +36,7 @@ import static jdk.jpackage.internal.StandardBundlerParam.*; import static jdk.jpackage.internal.MacAppBundler.*; +import static jdk.jpackage.internal.OverridableResource.createResource; public class MacAppStoreBundler extends MacBaseInstallerBundler { @@ -62,8 +60,7 @@ SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)); if (result != null) { - MacCertificate certificate = new MacCertificate(result, - VERBOSE.fetchFrom(params)); + MacCertificate certificate = new MacCertificate(result); if (!certificate.isValid()) { Log.error(MessageFormat.format( @@ -88,8 +85,7 @@ VERBOSE.fetchFrom(params)); if (result != null) { - MacCertificate certificate = new MacCertificate( - result, VERBOSE.fetchFrom(params)); + MacCertificate certificate = new MacCertificate(result); if (!certificate.isValid()) { Log.error(MessageFormat.format( @@ -209,40 +205,18 @@ private void prepareEntitlements(Map params) throws IOException { - File entitlements = MAC_APP_STORE_ENTITLEMENTS.fetchFrom(params); - if (entitlements == null || !entitlements.exists()) { - fetchResource(getEntitlementsFileName(params), - I18N.getString("resource.mac-app-store-entitlements"), - DEFAULT_ENTITLEMENTS, - getConfig_Entitlements(params), - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - } else { - fetchResource(getEntitlementsFileName(params), - I18N.getString("resource.mac-app-store-entitlements"), - entitlements, - getConfig_Entitlements(params), - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - } - fetchResource(getInheritEntitlementsFileName(params), - I18N.getString("resource.mac-app-store-inherit-entitlements"), - DEFAULT_INHERIT_ENTITLEMENTS, - getConfig_Inherit_Entitlements(params), - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); + createResource(DEFAULT_ENTITLEMENTS, params) + .setCategory( + I18N.getString("resource.mac-app-store-entitlements")) + .setExternal(MAC_APP_STORE_ENTITLEMENTS.fetchFrom(params)) + .saveToFile(getConfig_Entitlements(params)); + + createResource(DEFAULT_INHERIT_ENTITLEMENTS, params) + .setCategory(I18N.getString( + "resource.mac-app-store-inherit-entitlements")) + .saveToFile(getConfig_Entitlements(params)); } - private String getEntitlementsFileName(Map params) { - return APP_NAME.fetchFrom(params) + ".entitlements"; - } - - private String getInheritEntitlementsFileName( - Map params) { - return APP_NAME.fetchFrom(params) + "_Inherit.entitlements"; - } - - /////////////////////////////////////////////////////////////////////// // Implement Bundler /////////////////////////////////////////////////////////////////////// diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificate.java --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificate.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificate.java Wed Oct 16 09:57:23 2019 -0400 @@ -43,23 +43,16 @@ public final class MacCertificate { private final String certificate; - private final boolean verbose; public MacCertificate(String certificate) { this.certificate = certificate; - this.verbose = false; - } - - public MacCertificate(String certificate, boolean verbose) { - this.certificate = certificate; - this.verbose = verbose; } public boolean isValid() { - return verifyCertificate(this.certificate, verbose); + return verifyCertificate(this.certificate); } - private static File findCertificate(String certificate, boolean verbose) { + private static File findCertificate(String certificate) { File result = null; List args = new ArrayList<>(); @@ -87,7 +80,7 @@ return result; } - private static Date findCertificateDate(String filename, boolean verbose) { + private static Date findCertificateDate(String filename) { Date result = null; List args = new ArrayList<>(); @@ -114,8 +107,7 @@ return result; } - private static boolean verifyCertificate( - String certificate, boolean verbose) { + private static boolean verifyCertificate(String certificate) { boolean result = false; try { @@ -123,11 +115,11 @@ Date certificateDate = null; try { - file = findCertificate(certificate, verbose); + file = findCertificate(certificate); if (file != null) { certificateDate = findCertificateDate( - file.getCanonicalPath(), verbose); + file.getCanonicalPath()); } } finally { diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgBundler.java Wed Oct 16 09:57:23 2019 -0400 @@ -29,6 +29,7 @@ import java.nio.file.Files; import java.text.MessageFormat; import java.util.*; +import static jdk.jpackage.internal.OverridableResource.createResource; import static jdk.jpackage.internal.StandardBundlerParam.*; @@ -97,12 +98,10 @@ data.put("DEPLOY_INSTALL_LOCATION", "(path to applications folder)"); data.put("DEPLOY_INSTALL_NAME", "Applications"); - try (Writer w = Files.newBufferedWriter(dmgSetup.toPath())) { - w.write(preprocessTextResource(dmgSetup.getName(), - I18N.getString("resource.dmg-setup-script"), - DEFAULT_DMG_SETUP_SCRIPT, data, VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params))); - } + createResource(DEFAULT_DMG_SETUP_SCRIPT, params) + .setCategory(I18N.getString("resource.dmg-setup-script")) + .setSubstitutionData(data) + .saveToFile(dmgSetup); } private File getConfig_VolumeScript(Map params) { @@ -142,14 +141,10 @@ Map data = new HashMap<>(); data.put("APPLICATION_LICENSE_TEXT", licenseInBase64); - try (Writer w = Files.newBufferedWriter( - getConfig_LicenseFile(params).toPath())) { - w.write(preprocessTextResource( - getConfig_LicenseFile(params).getName(), - I18N.getString("resource.license-setup"), - DEFAULT_LICENSE_PLIST, data, VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params))); - } + createResource(DEFAULT_LICENSE_PLIST, params) + .setCategory(I18N.getString("resource.license-setup")) + .setSubstitutionData(data) + .saveToFile(getConfig_LicenseFile(params)); } catch (IOException ex) { Log.verbose(ex); @@ -158,39 +153,19 @@ private boolean prepareConfigFiles(Map params) throws IOException { - File bgTarget = getConfig_VolumeBackground(params); - fetchResource(bgTarget.getName(), - I18N.getString("resource.dmg-background"), - DEFAULT_BACKGROUND_IMAGE, - bgTarget, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); + + createResource(DEFAULT_BACKGROUND_IMAGE, params) + .setCategory(I18N.getString("resource.dmg-background")) + .saveToFile(getConfig_VolumeBackground(params)); - File iconTarget = getConfig_VolumeIcon(params); - if (MacAppBundler.ICON_ICNS.fetchFrom(params) == null || - !MacAppBundler.ICON_ICNS.fetchFrom(params).exists()) { - fetchResource(iconTarget.getName(), - I18N.getString("resource.volume-icon"), - TEMPLATE_BUNDLE_ICON, - iconTarget, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - } else { - fetchResource(iconTarget.getName(), - I18N.getString("resource.volume-icon"), - MacAppBundler.ICON_ICNS.fetchFrom(params), - iconTarget, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - } + createResource(TEMPLATE_BUNDLE_ICON, params) + .setCategory(I18N.getString("resource.volume-icon")) + .setExternal(MacAppBundler.ICON_ICNS.fetchFrom(params)) + .saveToFile(getConfig_VolumeIcon(params)); - - fetchResource(getConfig_Script(params).getName(), - I18N.getString("resource.post-install-script"), - (String) null, - getConfig_Script(params), - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); + createResource(null, params) + .setCategory(I18N.getString("resource.post-install-script")) + .saveToFile(getConfig_Script(params)); prepareLicense(params); diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgBundler.java Wed Oct 16 09:57:23 2019 -0400 @@ -50,6 +50,7 @@ import static jdk.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEYCHAIN; import static jdk.jpackage.internal.MacBaseInstallerBundler.SIGNING_KEY_USER; import static jdk.jpackage.internal.MacAppImageBuilder.MAC_CF_BUNDLE_IDENTIFIER; +import static jdk.jpackage.internal.OverridableResource.createResource; public class MacPkgBundler extends MacBaseInstallerBundler { @@ -100,8 +101,7 @@ SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)); if (result != null) { - MacCertificate certificate = new MacCertificate( - result, VERBOSE.fetchFrom(params)); + MacCertificate certificate = new MacCertificate(result); if (!certificate.isValid()) { Log.error(MessageFormat.format( @@ -206,30 +206,16 @@ data.put("INSTALL_LOCATION", MAC_INSTALL_DIR.fetchFrom(params)); data.put("APP_LOCATION", appLocation.toString()); - try (Writer w = Files.newBufferedWriter( - getScripts_PreinstallFile(params).toPath())) { - String content = preprocessTextResource( - getScripts_PreinstallFile(params).getName(), - I18N.getString("resource.pkg-preinstall-script"), - TEMPLATE_PREINSTALL_SCRIPT, - data, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - w.write(content); - } + createResource(TEMPLATE_PREINSTALL_SCRIPT, params) + .setCategory(I18N.getString("resource.pkg-preinstall-script")) + .setSubstitutionData(data) + .saveToFile(getScripts_PreinstallFile(params)); getScripts_PreinstallFile(params).setExecutable(true, false); - try (Writer w = Files.newBufferedWriter( - getScripts_PostinstallFile(params).toPath())) { - String content = preprocessTextResource( - getScripts_PostinstallFile(params).getName(), - I18N.getString("resource.pkg-postinstall-script"), - TEMPLATE_POSTINSTALL_SCRIPT, - data, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - w.write(content); - } + createResource(TEMPLATE_POSTINSTALL_SCRIPT, params) + .setCategory(I18N.getString("resource.pkg-postinstall-script")) + .setSubstitutionData(data) + .saveToFile(getScripts_PostinstallFile(params)); getScripts_PostinstallFile(params).setExecutable(true, false); } @@ -335,30 +321,20 @@ private boolean prepareConfigFiles(Map params) throws IOException, URISyntaxException { - File imageTarget = getConfig_BackgroundImage(params); - fetchResource(imageTarget.getName(), - I18N.getString("resource.pkg-background-image"), - DEFAULT_BACKGROUND_IMAGE, - imageTarget, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - imageTarget = getConfig_BackgroundImageDarkAqua(params); - fetchResource(imageTarget.getName(), - I18N.getString("resource.pkg-background-image"), - DEFAULT_BACKGROUND_IMAGE, - imageTarget, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); + createResource(DEFAULT_BACKGROUND_IMAGE, params) + .setCategory(I18N.getString("resource.pkg-background-image")) + .saveToFile(getConfig_BackgroundImage(params)); + + createResource(DEFAULT_BACKGROUND_IMAGE, params) + .setCategory(I18N.getString("resource.pkg-background-image")) + .saveToFile(getConfig_BackgroundImageDarkAqua(params)); prepareDistributionXMLFile(params); - fetchResource(getConfig_Script(params).getName(), - I18N.getString("resource.post-install-script"), - (String) null, - getConfig_Script(params), - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); + createResource(null, params) + .setCategory(I18N.getString("resource.post-install-script")) + .saveToFile(getConfig_Script(params)); return true; } diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractAppImageBuilder.java --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractAppImageBuilder.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractAppImageBuilder.java Wed Oct 16 09:57:23 2019 -0400 @@ -87,92 +87,6 @@ } } - protected InputStream locateResource(String publicName, String category, - String defaultName, File customFile, - boolean verbose, File publicRoot) throws IOException { - InputStream is = null; - boolean customFromClasspath = false; - boolean customFromFile = false; - if (publicName != null) { - if (publicRoot != null) { - File publicResource = new File(publicRoot, publicName); - if (publicResource.exists() && publicResource.isFile()) { - is = new FileInputStream(publicResource); - } - } else { - is = getResourceAsStream(publicName); - } - customFromClasspath = (is != null); - } - if (is == null && customFile != null) { - is = new FileInputStream(customFile); - customFromFile = (is != null); - } - if (is == null && defaultName != null) { - is = getResourceAsStream(defaultName); - } - if (verbose) { - String msg = null; - if (customFromClasspath) { - msg = MessageFormat.format(I18N.getString( - "message.using-custom-resource"), - category == null ? "" : "[" + category + "] ", publicName); - } else if (customFromFile) { - msg = MessageFormat.format(I18N.getString( - "message.using-custom-resource-from-file"), - category == null ? "" : "[" + category + "] ", - customFile.getAbsoluteFile()); - } else if (is != null) { - msg = MessageFormat.format(I18N.getString( - "message.using-default-resource"), - defaultName, - category == null ? "" : "[" + category + "] ", - publicName); - } else { - msg = MessageFormat.format(I18N.getString( - "message.no-default-resource"), - defaultName == null ? "" : defaultName, - category == null ? "" : "[" + category + "] ", - publicName); - } - if (msg != null) { - Log.verbose(msg); - } - } - return is; - } - - - protected String preprocessTextResource(String publicName, String category, - String defaultName, Map pairs, - boolean verbose, File publicRoot) throws IOException { - InputStream inp = locateResource(publicName, category, - defaultName, null, verbose, publicRoot); - if (inp == null) { - throw new RuntimeException( - "Module corrupt? No "+defaultName+" resource!"); - } - - try (InputStream is = inp) { - //read fully into memory - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - while ((length = is.read(buffer)) != -1) { - baos.write(buffer, 0, length); - } - - //substitute - String result = new String(baos.toByteArray()); - for (Map.Entry e : pairs.entrySet()) { - if (e.getValue() != null) { - result = result.replace(e.getKey(), e.getValue()); - } - } - return result; - } - } - public void writeCfgFile(Map params, File cfgFileName) throws IOException { cfgFileName.getParentFile().mkdirs(); diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractBundler.java --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractBundler.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractBundler.java Wed Oct 16 09:57:23 2019 -0400 @@ -25,31 +25,19 @@ package jdk.jpackage.internal; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.BufferedInputStream; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.StandardCopyOption; -import java.nio.file.Files; -import java.text.MessageFormat; import java.util.Map; -import java.util.ResourceBundle; -import jdk.jpackage.internal.resources.ResourceLocator; /** * AbstractBundler * - * This is the base class all Bundlers extend from. - * It contains methods and parameters common to all Bundlers. - * The concrete implementations are in the platform specific Bundlers. + * This is the base class all bundlers extend from. + * It contains methods and parameters common to all bundlers. + * The concrete implementations are in the platform specific bundlers. */ -public abstract class AbstractBundler implements Bundler { - - private static final ResourceBundle I18N = ResourceBundle.getBundle( - "jdk.jpackage.internal.resources.MainResources"); +abstract class AbstractBundler implements Bundler { static final BundlerParamInfo IMAGES_ROOT = new StandardBundlerParam<>( @@ -59,118 +47,6 @@ StandardBundlerParam.TEMP_ROOT.fetchFrom(params), "images"), (s, p) -> null); - public InputStream getResourceAsStream(String name) { - return ResourceLocator.class.getResourceAsStream(name); - } - - protected void fetchResource(String publicName, String category, - String defaultName, File result, boolean verbose, File publicRoot) - throws IOException { - - try (InputStream is = streamResource(publicName, category, - defaultName, verbose, publicRoot)) { - if (is != null) { - Files.copy(is, result.toPath(), - StandardCopyOption.REPLACE_EXISTING); - } else { - if (verbose) { - Log.verbose(MessageFormat.format(I18N.getString( - "message.no-default-resource"), - defaultName == null ? "" : defaultName, - category == null ? "" : "[" + category + "] ", - publicName)); - } - } - } - } - - protected void fetchResource(String publicName, String category, - File defaultFile, File result, boolean verbose, File publicRoot) - throws IOException { - - try (InputStream is = streamResource(publicName, category, - null, verbose, publicRoot)) { - if (is != null) { - Files.copy(is, result.toPath()); - } else { - IOUtils.copyFile(defaultFile, result); - if (verbose) { - Log.verbose(MessageFormat.format(I18N.getString( - "message.using-custom-resource-from-file"), - category == null ? "" : "[" + category + "] ", - defaultFile.getAbsoluteFile())); - } - } - } - } - - private InputStream streamResource(String publicName, String category, - String defaultName, boolean verbose, File publicRoot) - throws IOException { - boolean custom = false; - InputStream is = null; - if (publicName != null) { - if (publicRoot != null) { - File publicResource = new File(publicRoot, publicName); - if (publicResource.exists() && publicResource.isFile()) { - is = new BufferedInputStream( - new FileInputStream(publicResource)); - } - } else { - is = getResourceAsStream(publicName); - } - custom = (is != null); - } - if (is == null && defaultName != null) { - is = getResourceAsStream(defaultName); - } - if (verbose && is != null) { - String msg = null; - if (custom) { - msg = MessageFormat.format(I18N.getString( - "message.using-custom-resource"), - category == null ? - "" : "[" + category + "] ", publicName); - } else { - msg = MessageFormat.format(I18N.getString( - "message.using-default-resource"), - defaultName == null ? "" : defaultName, - category == null ? "" : "[" + category + "] ", - publicName); - } - Log.verbose(msg); - } - return is; - } - - protected String preprocessTextResource(String publicName, String category, - String defaultName, Map pairs, - boolean verbose, File publicRoot) throws IOException { - InputStream inp = streamResource( - publicName, category, defaultName, verbose, publicRoot); - if (inp == null) { - throw new RuntimeException( - "Jar corrupt? No " + defaultName + " resource!"); - } - - // read fully into memory - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - while ((length = inp.read(buffer)) != -1) { - baos.write(buffer, 0, length); - } - - // substitute - String result = new String(baos.toByteArray()); - for (Map.Entry e : pairs.entrySet()) { - if (e.getValue() != null) { - result = result.replace(e.getKey(), e.getValue()); - } - } - return result; - } - @Override public String toString() { return getName(); diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java Wed Oct 16 09:57:23 2019 -0400 @@ -144,7 +144,7 @@ return macAppImage(); } - throw new IllegalArgumentException("Unknown platform"); + throw Platform.throwUnknownPlatformError(); } static ApplicationLayout javaRuntime() { diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/share/classes/jdk/jpackage/internal/OverridableResource.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/OverridableResource.java Wed Oct 16 09:57:23 2019 -0400 @@ -0,0 +1,223 @@ +/* + * 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.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static jdk.jpackage.internal.StandardBundlerParam.RESOURCE_DIR; +import jdk.jpackage.internal.resources.ResourceLocator; + +/** + * Resource file that may have the default value supplied by jpackage. It can be + * overridden by a file from resource directory set with {@code --resource-dir} + * jpackage parameter. + * + * Resource has default name and public name. Default name is the name of a file + * in {@code jdk.jpackage.internal.resources} package that provides the default + * value of the resource. + * + * Public name is a path relative to resource directory to a file with custom + * value of the resource. + * + * Use #setPublicName to set the public name. + * + * If #setPublicName was not called, name of file passed in #saveToFile function + * will be used as a public name. + * + * Use #setExternal to set arbitrary file as a source of resource. If non-null + * value was passed in #setExternal call that value will be used as a path to file + * to copy in the destination file passed in #saveToFile function call. + */ +final class OverridableResource { + + OverridableResource(String defaultName) { + this.defaultName = defaultName; + } + + OverridableResource setSubstitutionData(Map v) { + if (v != null) { + // Disconnect `v` + substitutionData = new HashMap<>(v); + } else { + substitutionData = null; + } + return this; + } + + OverridableResource setCategory(String v) { + category = v; + return this; + } + + String getCategory() { + return category; + } + + OverridableResource setResourceDir(Path v) { + resourceDir = v; + return this; + } + + OverridableResource setResourceDir(File v) { + return setResourceDir(toPath(v)); + } + + /** + * Set name of file to look for in resource dir. + * + * @return this + */ + OverridableResource setPublicName(Path v) { + publicName = v; + return this; + } + + OverridableResource setPublicName(String v) { + return setPublicName(Path.of(v)); + } + + OverridableResource setExternal(Path v) { + externalPath = v; + return this; + } + + OverridableResource setExternal(File v) { + return setExternal(toPath(v)); + } + + void saveToFile(Path dest) throws IOException { + final String printableCategory; + if (category != null) { + printableCategory = String.format("[%s]", category); + } else { + printableCategory = ""; + } + + if (externalPath != null && externalPath.toFile().exists()) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.using-custom-resource-from-file"), + printableCategory, + externalPath.toAbsolutePath().normalize())); + + try (InputStream in = Files.newInputStream(externalPath)) { + processResourceStream(in, dest); + } + return; + } + + final Path resourceName = Optional.ofNullable(publicName).orElse( + dest.getFileName()); + + if (resourceDir != null) { + final Path customResource = resourceDir.resolve(resourceName); + if (customResource.toFile().exists()) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.using-custom-resource"), printableCategory, + resourceDir.normalize().toAbsolutePath().relativize( + customResource.normalize().toAbsolutePath()))); + + try (InputStream in = Files.newInputStream(customResource)) { + processResourceStream(in, dest); + } + return; + } + } + + if (defaultName != null) { + Log.verbose(MessageFormat.format( + I18N.getString("message.using-default-resource"), + defaultName, printableCategory, resourceName)); + + try (InputStream in = readDefault(defaultName)) { + processResourceStream(in, dest); + } + } + } + + void saveToFile(File dest) throws IOException { + saveToFile(dest.toPath()); + } + + static InputStream readDefault(String resourceName) { + return ResourceLocator.class.getResourceAsStream(resourceName); + } + + static OverridableResource createResource(String defaultName, + Map params) { + return new OverridableResource(defaultName).setResourceDir( + RESOURCE_DIR.fetchFrom(params)); + } + + private static List substitute(Stream lines, + Map substitutionData) { + return lines.map(line -> { + String result = line; + for (var entry : substitutionData.entrySet()) { + result = result.replace(entry.getKey(), Optional.ofNullable( + entry.getValue()).orElse("")); + } + return result; + }).collect(Collectors.toList()); + } + + private static Path toPath(File v) { + if (v != null) { + return v.toPath(); + } + return null; + } + + private void processResourceStream(InputStream rawResource, Path dest) + throws IOException { + if (substitutionData == null) { + Files.createDirectories(dest.getParent()); + Files.copy(rawResource, dest, StandardCopyOption.REPLACE_EXISTING); + } else { + // Utf8 in and out + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(rawResource, StandardCharsets.UTF_8))) { + Files.createDirectories(dest.getParent()); + Files.write(dest, substitute(reader.lines(), substitutionData)); + } + } + } + + private Map substitutionData; + private String category; + private Path resourceDir; + private Path publicName; + private Path externalPath; + private final String defaultName; +} diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/share/classes/jdk/jpackage/internal/Platform.java --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Platform.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Platform.java Wed Oct 16 09:57:23 2019 -0400 @@ -114,4 +114,8 @@ static boolean isLinux() { return getPlatform() == LINUX; } + + static RuntimeException throwUnknownPlatformError() { + throw new IllegalArgumentException("Unknown platform"); + } } diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java Wed Oct 16 09:57:23 2019 -0400 @@ -127,7 +127,7 @@ // Copy template msi wrapper next to msi file String exePath = msi.getAbsolutePath(); exePath = exePath.substring(0, exePath.lastIndexOf('.')) + ".exe"; - try (InputStream is = getResourceAsStream(EXE_WRAPPER_NAME)) { + try (InputStream is = OverridableResource.readDefault(EXE_WRAPPER_NAME)) { Files.copy(is, Path.of(exePath)); } // Embed msi in msi wrapper exe. diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java Wed Oct 16 09:57:23 2019 -0400 @@ -28,6 +28,7 @@ import java.io.*; import java.nio.charset.Charset; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.text.MessageFormat; import java.util.*; @@ -35,6 +36,7 @@ import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; +import static jdk.jpackage.internal.OverridableResource.createResource; import static jdk.jpackage.internal.WindowsBundlerParam.*; @@ -527,12 +529,12 @@ private void prepareBasicProjectConfig( Map params) throws IOException { - fetchResource(getConfig_Script(params).getName(), - I18N.getString("resource.post-install-script"), - (String) null, - getConfig_Script(params), - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); + + Path scriptPath = getConfig_Script(params).toPath(); + + createResource(null, params) + .setCategory(I18N.getString("resource.post-install-script")) + .saveToFile(scriptPath); } private static String relativePath(File basedir, File file) { @@ -663,7 +665,7 @@ if (INSTALLDIR_CHOOSER.fetchFrom(params)) { data.put("JpInstallDirChooser", "yes"); String fname = "wixhelper.dll"; - try (InputStream is = getResourceAsStream(fname)) { + try (InputStream is = OverridableResource.readDefault(fname)) { Files.copy(is, Paths.get( CONFIG_ROOT.fetchFrom(params).getAbsolutePath(), fname)); @@ -673,14 +675,14 @@ // Copy l10n files. for (String loc : Arrays.asList("en", "ja", "zh_CN")) { String fname = "MsiInstallerStrings_" + loc + ".wxl"; - try (InputStream is = getResourceAsStream(fname)) { + try (InputStream is = OverridableResource.readDefault(fname)) { Files.copy(is, Paths.get( CONFIG_ROOT.fetchFrom(params).getAbsolutePath(), fname)); } } - try (InputStream is = getResourceAsStream("main.wxs")) { + try (InputStream is = OverridableResource.readDefault("main.wxs")) { Files.copy(is, Paths.get( getConfig_ProjectFile(params).getAbsolutePath())); } diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java Wed Oct 16 09:57:23 2019 -0400 @@ -50,6 +50,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import java.util.stream.Stream; +import static jdk.jpackage.internal.OverridableResource.createResource; import static jdk.jpackage.internal.StandardBundlerParam.*; @@ -265,35 +266,21 @@ validateValueAndPut(data, "PRODUCT_NAME", APP_NAME, params); validateValueAndPut(data, "PRODUCT_VERSION", VERSION, params); - try (Writer w = Files.newBufferedWriter( - getConfig_ExecutableProperties(params).toPath(), - StandardCharsets.UTF_8)) { - String content = preprocessTextResource( - getConfig_ExecutableProperties(params).getName(), - I18N.getString("resource.executable-properties-template"), - EXECUTABLE_PROPERTIES_TEMPLATE, data, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - w.write(content); - } + createResource(EXECUTABLE_PROPERTIES_TEMPLATE, params) + .setCategory(I18N.getString("resource.executable-properties-template")) + .setSubstitutionData(data) + .saveToFile(getConfig_ExecutableProperties(params)); } private void createLauncherForEntryPoint( Map params) throws IOException { - File icon = ICON_ICO.fetchFrom(params); File iconTarget = getConfig_AppIcon(params); - InputStream in = locateResource( - iconTarget.getName(), - "icon", - TEMPLATE_APP_ICON, - icon, - VERBOSE.fetchFrom(params), - RESOURCE_DIR.fetchFrom(params)); - - Files.copy(in, iconTarget.toPath(), - StandardCopyOption.REPLACE_EXISTING); + createResource(TEMPLATE_APP_ICON, params) + .setCategory("icon") + .setExternal(ICON_ICO.fetchFrom(params)) + .saveToFile(iconTarget); writeCfgFile(params, root.resolve( getLauncherCfgName(params)).toFile()); diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties Wed Oct 16 09:57:23 2019 -0400 @@ -30,7 +30,7 @@ param.menu-group.default=Unknown -resource.executable-properties-template=Template for creating executable properties file. +resource.executable-properties-template=Template for creating executable properties file resource.setup-icon=setup dialog icon resource.post-install-script=script to run after application image is populated resource.wxl-file-name=MsiInstallerStrings_en.wxl diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_ja.properties --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_ja.properties Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_ja.properties Wed Oct 16 09:57:23 2019 -0400 @@ -30,7 +30,7 @@ param.menu-group.default=Unknown -resource.executable-properties-template=Template for creating executable properties file. +resource.executable-properties-template=Template for creating executable properties file resource.setup-icon=setup dialog icon resource.post-install-script=script to run after application image is populated resource.wxl-file-name=MsiInstallerStrings_en.wxl diff -r a561014c28d0 -r 2c43b89b1679 src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_zh_CN.properties --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_zh_CN.properties Tue Oct 15 14:00:04 2019 -0400 +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources_zh_CN.properties Wed Oct 16 09:57:23 2019 -0400 @@ -30,7 +30,7 @@ param.menu-group.default=Unknown -resource.executable-properties-template=Template for creating executable properties file. +resource.executable-properties-template=Template for creating executable properties file resource.setup-icon=setup dialog icon resource.post-install-script=script to run after application image is populated resource.wxl-file-name=MsiInstallerStrings_en.wxl diff -r a561014c28d0 -r 2c43b89b1679 test/jdk/tools/jpackage/junit/jdk/jpackage/internal/OverridableResourceTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/OverridableResourceTest.java Wed Oct 16 09:57:23 2019 -0400 @@ -0,0 +1,226 @@ +/* + * 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.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import jdk.jpackage.internal.resources.ResourceLocator; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class OverridableResourceTest { + + @Rule + public final TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testDefault() throws IOException { + byte[] actualBytes = saveToFile(new OverridableResource(DEFAULT_NAME)); + + try (InputStream is = ResourceLocator.class.getResourceAsStream( + DEFAULT_NAME)) { + assertArrayEquals(is.readAllBytes(), actualBytes); + } + } + + @Test + public void testDefaultWithSubstitution() throws IOException { + OverridableResource resource = new OverridableResource(DEFAULT_NAME); + + List linesBeforeSubstitution = convertToStringList(saveToFile( + resource)); + + if (SUBSTITUTION_DATA.size() != 1) { + // Test setup issue + throw new IllegalArgumentException( + "Substitution map should contain only a single entry"); + } + + resource.setSubstitutionData(SUBSTITUTION_DATA); + List linesAfterSubstitution = convertToStringList(saveToFile( + resource)); + + assertEquals(linesBeforeSubstitution.size(), linesAfterSubstitution.size()); + + Iterator beforeIt = linesBeforeSubstitution.iterator(); + Iterator afterIt = linesAfterSubstitution.iterator(); + + var substitutionEntry = SUBSTITUTION_DATA.entrySet().iterator().next(); + + boolean linesMismatch = false; + while (beforeIt.hasNext()) { + String beforeStr = beforeIt.next(); + String afterStr = afterIt.next(); + + if (beforeStr.equals(afterStr)) { + assertFalse(beforeStr.contains(substitutionEntry.getKey())); + } else { + linesMismatch = true; + assertTrue(beforeStr.contains(substitutionEntry.getKey())); + assertTrue(afterStr.contains(substitutionEntry.getValue())); + assertFalse(afterStr.contains(substitutionEntry.getKey())); + } + } + + assertTrue(linesMismatch); + } + + @Test + public void testCustom() throws IOException { + testCustom(DEFAULT_NAME); + } + + @Test + public void testCustomNoDefault() throws IOException { + testCustom(null); + } + + private void testCustom(String defaultName) throws IOException { + List expectedResourceData = List.of("A", "B", "C"); + + Path customFile = createCustomFile("foo", expectedResourceData); + + List actualResourceData = convertToStringList(saveToFile( + new OverridableResource(defaultName) + .setPublicName(customFile.getFileName()) + .setResourceDir(customFile.getParent()))); + + assertArrayEquals(expectedResourceData.toArray(String[]::new), + actualResourceData.toArray(String[]::new)); + } + + @Test + public void testCustomtWithSubstitution() throws IOException { + testCustomtWithSubstitution(DEFAULT_NAME); + } + + @Test + public void testCustomtWithSubstitutionNoDefault() throws IOException { + testCustomtWithSubstitution(null); + } + + private void testCustomtWithSubstitution(String defaultName) throws IOException { + final List resourceData = List.of("A", "[BB]", "C", "Foo", + "GoodbyeHello"); + final Path customFile = createCustomFile("foo", resourceData); + + final Map substitutionData = new HashMap(Map.of("B", + "Bar", "Foo", "B")); + substitutionData.put("Hello", null); + + final List expectedResourceData = List.of("A", "[BarBar]", "C", + "B", "Goodbye"); + + final List actualResourceData = convertToStringList(saveToFile( + new OverridableResource(defaultName) + .setPublicName(customFile.getFileName()) + .setSubstitutionData(substitutionData) + .setResourceDir(customFile.getParent()))); + assertArrayEquals(expectedResourceData.toArray(String[]::new), + actualResourceData.toArray(String[]::new)); + + // Don't call setPublicName() + final Path dstFile = tempFolder.newFolder().toPath().resolve(customFile.getFileName()); + new OverridableResource(defaultName) + .setSubstitutionData(substitutionData) + .setResourceDir(customFile.getParent()) + .saveToFile(dstFile); + assertArrayEquals(expectedResourceData.toArray(String[]::new), + convertToStringList(Files.readAllBytes(dstFile)).toArray( + String[]::new)); + + // Verify setSubstitutionData() stores a copy of passed in data + Map substitutionData2 = new HashMap(substitutionData); + var resource = new OverridableResource(defaultName) + .setResourceDir(customFile.getParent()); + + resource.setSubstitutionData(substitutionData2); + substitutionData2.clear(); + Files.delete(dstFile); + resource.saveToFile(dstFile); + assertArrayEquals(expectedResourceData.toArray(String[]::new), + convertToStringList(Files.readAllBytes(dstFile)).toArray( + String[]::new)); + } + + @Test + public void testNoDefault() throws IOException { + Path dstFolder = tempFolder.newFolder().toPath(); + Path dstFile = dstFolder.resolve(Path.of("foo", "bar")); + + new OverridableResource(null).saveToFile(dstFile); + + assertFalse(dstFile.toFile().exists()); + } + + private final static String DEFAULT_NAME; + private final static Map SUBSTITUTION_DATA; + static { + if (Platform.isWindows()) { + DEFAULT_NAME = "WinLauncher.template"; + SUBSTITUTION_DATA = Map.of("COMPANY_NAME", "Foo9090345"); + } else if (Platform.isLinux()) { + DEFAULT_NAME = "template.control"; + SUBSTITUTION_DATA = Map.of("APPLICATION_PACKAGE", "Package1967"); + } else if (Platform.isMac()) { + DEFAULT_NAME = "Info-lite.plist.template"; + SUBSTITUTION_DATA = Map.of("DEPLOY_BUNDLE_IDENTIFIER", "12345"); + } else { + throw Platform.throwUnknownPlatformError(); + } + } + + private byte[] saveToFile(OverridableResource resource) throws IOException { + Path dstFile = tempFolder.newFile().toPath(); + resource.saveToFile(dstFile); + assertThat(0, is(not(dstFile.toFile().length()))); + + return Files.readAllBytes(dstFile); + } + + private Path createCustomFile(String publicName, List data) throws + IOException { + Path resourceFolder = tempFolder.newFolder().toPath(); + Path customFile = resourceFolder.resolve(publicName); + + Files.write(customFile, data); + + return customFile; + } + + private static List convertToStringList(byte[] data) { + return List.of(new String(data, StandardCharsets.UTF_8).split("\\R")); + } +} diff -r a561014c28d0 -r 2c43b89b1679 test/jdk/tools/jpackage/junit/junit.java --- a/test/jdk/tools/jpackage/junit/junit.java Tue Oct 15 14:00:04 2019 -0400 +++ b/test/jdk/tools/jpackage/junit/junit.java Wed Oct 16 09:57:23 2019 -0400 @@ -28,9 +28,4 @@ * @summary jpackage unit tests * @library ${jtreg.home}/lib/junit.jar * @run shell run_junit.sh - * jdk.jpackage.internal.PathGroupTest - * jdk.jpackage.internal.DeployParamsTest - * jdk.jpackage.internal.ApplicationLayoutTest - * jdk.jpackage.internal.ToolValidatorTest - * jdk.jpackage.internal.AppImageFileTest */ diff -r a561014c28d0 -r 2c43b89b1679 test/jdk/tools/jpackage/junit/run_junit.sh --- a/test/jdk/tools/jpackage/junit/run_junit.sh Tue Oct 15 14:00:04 2019 -0400 +++ b/test/jdk/tools/jpackage/junit/run_junit.sh Wed Oct 16 09:57:23 2019 -0400 @@ -9,10 +9,11 @@ exit fi -classes=( "$@" ) sources=() -for c in "${classes[@]}"; do - sources+=( "${TESTSRC}/$(echo $c | sed -e 's|\.|/|g').java" ) +classes=() +for s in $(find "${TESTSRC}" -name "*.java" | grep -v junit.java); do + sources+=( "$s" ) + classes+=( $(echo "$s" | sed -e "s|${TESTSRC}/||" -e 's|/|.|g' -e 's/.java$//') ) done common_args=(\ @@ -28,4 +29,4 @@ # Run junit "${TESTJAVA}/bin/java" ${TESTVMOPTS} ${TESTJAVAOPTS} \ - "${common_args[@]}" org.junit.runner.JUnitCore "$@" + "${common_args[@]}" org.junit.runner.JUnitCore "${classes[@]}" diff -r a561014c28d0 -r 2c43b89b1679 test/jdk/tools/jpackage/macosx/base/SigningCheck.java --- a/test/jdk/tools/jpackage/macosx/base/SigningCheck.java Tue Oct 15 14:00:04 2019 -0400 +++ b/test/jdk/tools/jpackage/macosx/base/SigningCheck.java Wed Oct 16 09:57:23 2019 -0400 @@ -72,8 +72,7 @@ private static void validateCertificate(String key) { if (key != null) { - MacCertificate certificate = new MacCertificate( - key, true); + MacCertificate certificate = new MacCertificate(key); if (!certificate.isValid()) { TKit.throwSkippedException("Certifcate expired: " + key); } else {