--- /dev/null 2019-11-13 18:18:51.000000000 -0500 +++ new/src/jdk.incubator.jpackage/windows/classes/jdk/incubator/jpackage/internal/WixSourcesBuilder.java 2019-11-13 18:18:48.139197900 -0500 @@ -0,0 +1,847 @@ +/* + * 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.incubator.jpackage.internal; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.*; +import java.util.function.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import jdk.incubator.jpackage.internal.IOUtils.XmlConsumer; +import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; +import static jdk.incubator.jpackage.internal.WinMsiBundler.*; +import static jdk.incubator.jpackage.internal.WindowsBundlerParam.MENU_GROUP; +import static jdk.incubator.jpackage.internal.WindowsBundlerParam.WINDOWS_INSTALL_DIR; + +/** + * Creates application WiX source files. + */ +class WixSourcesBuilder { + + WixSourcesBuilder setWixVersion(DottedVersion v) { + wixVersion = v; + return this; + } + + WixSourcesBuilder initFromParams(Path appImageRoot, + Map params) { + Supplier appImageSupplier = () -> { + if (StandardBundlerParam.isRuntimeInstaller(params)) { + return ApplicationLayout.javaRuntime(); + } else { + return ApplicationLayout.platformAppImage(); + } + }; + + systemWide = MSI_SYSTEM_WIDE.fetchFrom(params); + + registryKeyPath = Path.of("Software", + VENDOR.fetchFrom(params), + APP_NAME.fetchFrom(params), + VERSION.fetchFrom(params)).toString(); + + installDir = (systemWide ? PROGRAM_FILES : LOCAL_PROGRAM_FILES).resolve( + WINDOWS_INSTALL_DIR.fetchFrom(params)); + + do { + ApplicationLayout layout = appImageSupplier.get(); + // Don't want AppImageFile.FILENAME in installed application. + // Register it with app image at a role without a match in installed + // app layout to exclude it from layout transformation. + layout.pathGroup().setPath(new Object(), + AppImageFile.getPathInAppImage(Path.of(""))); + + // Want absolute paths to source files in generated WiX sources. + // This is to handle scenario if sources would be processed from + // differnt current directory. + appImage = layout.resolveAt(appImageRoot.toAbsolutePath().normalize()); + } while (false); + + installedAppImage = appImageSupplier.get().resolveAt(INSTALLDIR); + + shortcutFolders = new HashSet<>(); + if (SHORTCUT_HINT.fetchFrom(params)) { + shortcutFolders.add(ShortcutsFolder.Desktop); + } + if (MENU_HINT.fetchFrom(params)) { + shortcutFolders.add(ShortcutsFolder.ProgramMenu); + } + + if (StandardBundlerParam.isRuntimeInstaller(params)) { + launcherPaths = Collections.emptyList(); + } else { + launcherPaths = AppImageFile.getLauncherNames(appImageRoot, params).stream() + .map(name -> installedAppImage.launchersDirectory().resolve(name)) + .map(WixSourcesBuilder::addExeSuffixToPath) + .collect(Collectors.toList()); + } + + programMenuFolderName = MENU_GROUP.fetchFrom(params); + + initFileAssociations(params); + + return this; + } + + void createMainFragment(Path file) throws IOException { + removeFolderItems = new HashMap<>(); + defaultedMimes = new HashSet<>(); + IOUtils.createXml(file, xml -> { + xml.writeStartElement("Wix"); + xml.writeDefaultNamespace("http://schemas.microsoft.com/wix/2006/wi"); + xml.writeNamespace("util", + "http://schemas.microsoft.com/wix/UtilExtension"); + + xml.writeStartElement("Fragment"); + + addFaComponentGroup(xml); + + addShortcutComponentGroup(xml); + + addFilesComponentGroup(xml); + + xml.writeEndElement(); // + + addIconsFragment(xml); + + xml.writeEndElement(); // + }); + } + + void logWixFeatures() { + if (wixVersion.compareTo("3.6") >= 0) { + Log.verbose(MessageFormat.format(I18N.getString( + "message.use-wix36-features"), wixVersion)); + } + } + + private void normalizeFileAssociation(FileAssociation fa) { + fa.launcherPath = addExeSuffixToPath( + installedAppImage.launchersDirectory().resolve(fa.launcherPath)); + + if (fa.iconPath != null && !fa.iconPath.toFile().exists()) { + fa.iconPath = null; + } + + if (fa.iconPath != null) { + fa.iconPath = fa.iconPath.toAbsolutePath(); + } + + // Filter out empty extensions. + fa.extensions = fa.extensions.stream().filter(Predicate.not( + String::isEmpty)).collect(Collectors.toList()); + } + + private static Path addExeSuffixToPath(Path path) { + return IOUtils.addSuffix(path, ".exe"); + } + + private Path getInstalledFaIcoPath(FileAssociation fa) { + String fname = String.format("fa_%s.ico", String.join("_", fa.extensions)); + return installedAppImage.destktopIntegrationDirectory().resolve(fname); + } + + private void initFileAssociations(Map params) { + associations = FileAssociation.fetchFrom(params).stream() + .peek(this::normalizeFileAssociation) + // Filter out file associations without extensions. + .filter(fa -> !fa.extensions.isEmpty()) + .collect(Collectors.toList()); + + associations.stream().filter(fa -> fa.iconPath != null).forEach(fa -> { + // Need to add fa icon in the image. + Object key = new Object(); + appImage.pathGroup().setPath(key, fa.iconPath); + installedAppImage.pathGroup().setPath(key, getInstalledFaIcoPath(fa)); + }); + } + + private static UUID createNameUUID(String str) { + return UUID.nameUUIDFromBytes(str.getBytes(StandardCharsets.UTF_8)); + } + + private static UUID createNameUUID(Path path, String role) { + if (path.isAbsolute() || !ROOT_DIRS.contains(path.getName(0))) { + throw throwInvalidPathException(path); + } + // Paths are case insensitive on Windows + String keyPath = path.toString().toLowerCase(); + if (role != null) { + keyPath = role + "@" + keyPath; + } + return createNameUUID(keyPath); + } + + /** + * Value for Id attribute of various WiX elements. + */ + enum Id { + File, + Folder("dir"), + Shortcut, + ProgId, + Icon, + CreateFolder("mkdir"), + RemoveFolder("rm"); + + Id() { + this.prefix = name().toLowerCase(); + } + + Id(String prefix) { + this.prefix = prefix; + } + + String of(Path path) { + if (this == Folder && KNOWN_DIRS.contains(path)) { + return path.getFileName().toString(); + } + + String result = of(path, prefix, name()); + + if (this == Icon) { + // Icon id constructed from UUID value is too long and triggers + // CNDL1000 warning, so use Java hash code instead. + result = String.format("%s%d", prefix, result.hashCode()).replace( + "-", "_"); + } + + return result; + } + + private static String of(Path path, String prefix, String role) { + Objects.requireNonNull(role); + Objects.requireNonNull(prefix); + return String.format("%s%s", prefix, + createNameUUID(path, role).toString().replace("-", "")); + } + + static String of(Path path, String prefix) { + return of(path, prefix, prefix); + } + + private final String prefix; + } + + enum Component { + File(cfg().file()), + Shortcut(cfg().file().withRegistryKeyPath()), + ProgId(cfg().file().withRegistryKeyPath()), + CreateFolder(cfg().withRegistryKeyPath()), + RemoveFolder(cfg().withRegistryKeyPath()); + + Component() { + this.cfg = cfg(); + this.id = Id.valueOf(name()); + } + + Component(Config cfg) { + this.cfg = cfg; + this.id = Id.valueOf(name()); + } + + UUID guidOf(Path path) { + return createNameUUID(path, name()); + } + + String idOf(Path path) { + return id.of(path); + } + + boolean isRegistryKeyPath() { + return cfg.withRegistryKeyPath; + } + + boolean isFile() { + return cfg.isFile; + } + + static void startElement(XMLStreamWriter xml, String componentId, + String componentGuid) throws XMLStreamException, IOException { + xml.writeStartElement("Component"); + xml.writeAttribute("Win64", "yes"); + xml.writeAttribute("Id", componentId); + xml.writeAttribute("Guid", componentGuid); + } + + private static final class Config { + Config withRegistryKeyPath() { + withRegistryKeyPath = true; + return this; + } + + Config file() { + isFile = true; + return this; + } + + private boolean isFile; + private boolean withRegistryKeyPath; + } + + private static Config cfg() { + return new Config(); + } + + private final Config cfg; + private final Id id; + }; + + private static void addComponentGroup(XMLStreamWriter xml, String id, + List componentIds) throws XMLStreamException, IOException { + xml.writeStartElement("ComponentGroup"); + xml.writeAttribute("Id", id); + componentIds = componentIds.stream().filter(Objects::nonNull).collect( + Collectors.toList()); + for (var componentId : componentIds) { + xml.writeStartElement("ComponentRef"); + xml.writeAttribute("Id", componentId); + xml.writeEndElement(); + } + xml.writeEndElement(); + } + + private String addComponent(XMLStreamWriter xml, Path path, + Component role, XmlConsumer xmlConsumer) throws XMLStreamException, + IOException { + + final Path directoryRefPath; + if (role.isFile()) { + directoryRefPath = path.getParent(); + } else { + directoryRefPath = path; + } + + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", Id.Folder.of(directoryRefPath)); + + final String componentId = "c" + role.idOf(path); + Component.startElement(xml, componentId, String.format("{%s}", + role.guidOf(path))); + + boolean isRegistryKeyPath = !systemWide || role.isRegistryKeyPath(); + if (isRegistryKeyPath) { + addRegistryKeyPath(xml, directoryRefPath); + if ((role.isFile() || (role == Component.CreateFolder + && !systemWide)) && !SYSTEM_DIRS.contains(directoryRefPath)) { + xml.writeStartElement("RemoveFolder"); + int counter = Optional.ofNullable(removeFolderItems.get( + directoryRefPath)).orElse(Integer.valueOf(0)).intValue() + 1; + removeFolderItems.put(directoryRefPath, counter); + xml.writeAttribute("Id", String.format("%s_%d", Id.RemoveFolder.of( + directoryRefPath), counter)); + xml.writeAttribute("On", "uninstall"); + xml.writeEndElement(); + } + } + + xml.writeStartElement(role.name()); + if (role != Component.CreateFolder) { + xml.writeAttribute("Id", role.idOf(path)); + } + + if (!isRegistryKeyPath) { + xml.writeAttribute("KeyPath", "yes"); + } + + xmlConsumer.accept(xml); + xml.writeEndElement(); + + xml.writeEndElement(); // + xml.writeEndElement(); // + + return componentId; + } + + private void addFaComponentGroup(XMLStreamWriter xml) + throws XMLStreamException, IOException { + + List componentIds = new ArrayList<>(); + for (var fa : associations) { + componentIds.addAll(addFaComponents(xml, fa)); + } + addComponentGroup(xml, "FileAssociations", componentIds); + } + + private void addShortcutComponentGroup(XMLStreamWriter xml) throws + XMLStreamException, IOException { + List componentIds = new ArrayList<>(); + Set defineShortcutFolders = new HashSet<>(); + for (var launcherPath : launcherPaths) { + for (var folder : shortcutFolders) { + String componentId = addShortcutComponent(xml, launcherPath, + folder); + if (componentId != null) { + defineShortcutFolders.add(folder); + componentIds.add(componentId); + } + } + } + + for (var folder : defineShortcutFolders) { + Path path = folder.getPath(this); + componentIds.addAll(addRootBranch(xml, path)); + } + + addComponentGroup(xml, "Shortcuts", componentIds); + } + + private String addShortcutComponent(XMLStreamWriter xml, Path launcherPath, + ShortcutsFolder folder) throws XMLStreamException, IOException { + Objects.requireNonNull(folder); + + if (!INSTALLDIR.equals(launcherPath.getName(0))) { + throw throwInvalidPathException(launcherPath); + } + + String launcherBasename = IOUtils.replaceSuffix( + launcherPath.getFileName(), "").toString(); + + Path shortcutPath = folder.getPath(this).resolve(launcherBasename); + return addComponent(xml, shortcutPath, Component.Shortcut, unused -> { + final Path icoFile = IOUtils.addSuffix( + installedAppImage.destktopIntegrationDirectory().resolve( + launcherBasename), ".ico"); + + xml.writeAttribute("Name", launcherBasename); + xml.writeAttribute("WorkingDirectory", INSTALLDIR.toString()); + xml.writeAttribute("Advertise", "no"); + xml.writeAttribute("IconIndex", "0"); + xml.writeAttribute("Target", String.format("[#%s]", + Component.File.idOf(launcherPath))); + xml.writeAttribute("Icon", Id.Icon.of(icoFile)); + }); + } + + private List addFaComponents(XMLStreamWriter xml, + FileAssociation fa) throws XMLStreamException, IOException { + List components = new ArrayList<>(); + for (var extension: fa.extensions) { + Path path = INSTALLDIR.resolve(String.format("%s_%s", extension, + fa.launcherPath.getFileName())); + components.add(addComponent(xml, path, Component.ProgId, unused -> { + xml.writeAttribute("Description", fa.description); + + if (fa.iconPath != null) { + xml.writeAttribute("Icon", Id.File.of(getInstalledFaIcoPath( + fa))); + xml.writeAttribute("IconIndex", "0"); + } + + xml.writeStartElement("Extension"); + xml.writeAttribute("Id", extension); + xml.writeAttribute("Advertise", "no"); + + var mimeIt = fa.mimeTypes.iterator(); + if (mimeIt.hasNext()) { + String mime = mimeIt.next(); + xml.writeAttribute("ContentType", mime); + + if (!defaultedMimes.contains(mime)) { + xml.writeStartElement("MIME"); + xml.writeAttribute("ContentType", mime); + xml.writeAttribute("Default", "yes"); + xml.writeEndElement(); + defaultedMimes.add(mime); + } + } + + xml.writeStartElement("Verb"); + xml.writeAttribute("Id", "open"); + xml.writeAttribute("Command", "Open"); + xml.writeAttribute("Argument", "%1"); + xml.writeAttribute("TargetFile", Id.File.of(fa.launcherPath)); + xml.writeEndElement(); // + + xml.writeEndElement(); // + })); + } + + return components; + } + + private List addRootBranch(XMLStreamWriter xml, Path path) + throws XMLStreamException, IOException { + if (!ROOT_DIRS.contains(path.getName(0))) { + throw throwInvalidPathException(path); + } + + Function createDirectoryName = dir -> null; + + boolean sysDir = true; + int levels = 1; + var dirIt = path.iterator(); + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", dirIt.next().toString()); + + path = path.getName(0); + while (dirIt.hasNext()) { + levels++; + Path name = dirIt.next(); + path = path.resolve(name); + + if (sysDir && !SYSTEM_DIRS.contains(path)) { + sysDir = false; + createDirectoryName = dir -> dir.getFileName().toString(); + } + + final String directoryId; + if (!sysDir && path.equals(installDir)) { + directoryId = INSTALLDIR.toString(); + } else { + directoryId = Id.Folder.of(path); + } + xml.writeStartElement("Directory"); + xml.writeAttribute("Id", directoryId); + + String directoryName = createDirectoryName.apply(path); + if (directoryName != null) { + xml.writeAttribute("Name", directoryName); + } + } + + while (0 != levels--) { + xml.writeEndElement(); + } + + List componentIds = new ArrayList<>(); + while (!SYSTEM_DIRS.contains(path = path.getParent())) { + componentIds.add(addRemoveDirectoryComponent(xml, path)); + } + + return componentIds; + } + + private String addRemoveDirectoryComponent(XMLStreamWriter xml, Path path) + throws XMLStreamException, IOException { + return addComponent(xml, path, Component.RemoveFolder, + unused -> xml.writeAttribute("On", "uninstall")); + } + + private List addDirectoryHierarchy(XMLStreamWriter xml) + throws XMLStreamException, IOException { + + Set allDirs = new HashSet<>(); + Set emptyDirs = new HashSet<>(); + appImage.transform(installedAppImage, new PathGroup.TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + Path dir = dst.getParent(); + createDirectory(dir); + emptyDirs.remove(dir); + } + + @Override + public void createDirectory(final Path dir) throws IOException { + if (!allDirs.contains(dir)) { + emptyDirs.add(dir); + } + + Path it = dir; + while (it != null && allDirs.add(it)) { + it = it.getParent(); + } + + it = dir; + while ((it = it.getParent()) != null && emptyDirs.remove(it)); + } + }); + + List componentIds = new ArrayList<>(); + for (var dir : emptyDirs) { + componentIds.add(addComponent(xml, dir, Component.CreateFolder, + unused -> {})); + } + + if (!systemWide) { + // Per-user install requires component in every + // directory. + for (var dir : allDirs.stream() + .filter(Predicate.not(emptyDirs::contains)) + .filter(Predicate.not(removeFolderItems::containsKey)) + .collect(Collectors.toList())) { + componentIds.add(addRemoveDirectoryComponent(xml, dir)); + } + } + + allDirs.remove(INSTALLDIR); + for (var dir : allDirs) { + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", Id.Folder.of(dir.getParent())); + xml.writeStartElement("Directory"); + xml.writeAttribute("Id", Id.Folder.of(dir)); + xml.writeAttribute("Name", dir.getFileName().toString()); + xml.writeEndElement(); + xml.writeEndElement(); + } + + componentIds.addAll(addRootBranch(xml, installDir)); + + return componentIds; + } + + private void addFilesComponentGroup(XMLStreamWriter xml) + throws XMLStreamException, IOException { + + List> files = new ArrayList<>(); + appImage.transform(installedAppImage, new PathGroup.TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + files.add(Map.entry(src, dst)); + } + + @Override + public void createDirectory(final Path dir) throws IOException { + } + }); + + List componentIds = new ArrayList<>(); + for (var file : files) { + Path src = file.getKey(); + Path dst = file.getValue(); + + componentIds.add(addComponent(xml, dst, Component.File, unused -> { + xml.writeAttribute("Source", src.normalize().toString()); + Path name = dst.getFileName(); + if (!name.equals(src.getFileName())) { + xml.writeAttribute("Name", name.toString()); + } + })); + } + + componentIds.addAll(addDirectoryHierarchy(xml)); + + componentIds.add(addDirectoryCleaner(xml, INSTALLDIR)); + + addComponentGroup(xml, "Files", componentIds); + } + + private void addIconsFragment(XMLStreamWriter xml) throws + XMLStreamException, IOException { + + PathGroup srcPathGroup = appImage.pathGroup(); + PathGroup dstPathGroup = installedAppImage.pathGroup(); + + // Build list of copy operations for all .ico files in application image + List> icoFiles = new ArrayList<>(); + srcPathGroup.transform(dstPathGroup, new PathGroup.TransformHandler() { + @Override + public void copyFile(Path src, Path dst) throws IOException { + if (src.getFileName().toString().endsWith(".ico")) { + icoFiles.add(Map.entry(src, dst)); + } + } + + @Override + public void createDirectory(Path dst) throws IOException { + } + }); + + xml.writeStartElement("Fragment"); + for (var icoFile : icoFiles) { + xml.writeStartElement("Icon"); + xml.writeAttribute("Id", Id.Icon.of(icoFile.getValue())); + xml.writeAttribute("SourceFile", icoFile.getKey().toString()); + xml.writeEndElement(); + } + xml.writeEndElement(); + } + + private void addRegistryKeyPath(XMLStreamWriter xml, Path path) throws + XMLStreamException, IOException { + addRegistryKeyPath(xml, path, () -> "ProductCode", () -> "[ProductCode]"); + } + + private void addRegistryKeyPath(XMLStreamWriter xml, Path path, + Supplier nameAttr, Supplier valueAttr) throws + XMLStreamException, IOException { + + String regRoot = USER_PROFILE_DIRS.stream().anyMatch(path::startsWith) + || !systemWide ? "HKCU" : "HKLM"; + + xml.writeStartElement("RegistryKey"); + xml.writeAttribute("Root", regRoot); + xml.writeAttribute("Key", registryKeyPath); + if (wixVersion.compareTo("3.6") < 0) { + xml.writeAttribute("Action", "createAndRemoveOnUninstall"); + } + xml.writeStartElement("RegistryValue"); + xml.writeAttribute("Type", "string"); + xml.writeAttribute("KeyPath", "yes"); + xml.writeAttribute("Name", nameAttr.get()); + xml.writeAttribute("Value", valueAttr.get()); + xml.writeEndElement(); // + xml.writeEndElement(); // + } + + private String addDirectoryCleaner(XMLStreamWriter xml, Path path) throws + XMLStreamException, IOException { + if (wixVersion.compareTo("3.6") < 0) { + return null; + } + + // rm -rf + final String baseId = Id.of(path, "rm_rf"); + final String propertyId = baseId.toUpperCase(); + final String componentId = ("c" + baseId); + + xml.writeStartElement("Property"); + xml.writeAttribute("Id", propertyId); + xml.writeStartElement("RegistrySearch"); + xml.writeAttribute("Id", Id.of(path, "regsearch")); + xml.writeAttribute("Root", systemWide ? "HKLM" : "HKCU"); + xml.writeAttribute("Key", registryKeyPath); + xml.writeAttribute("Type", "raw"); + xml.writeAttribute("Name", propertyId); + xml.writeEndElement(); // + xml.writeEndElement(); // + + xml.writeStartElement("DirectoryRef"); + xml.writeAttribute("Id", INSTALLDIR.toString()); + Component.startElement(xml, componentId, "*"); + + addRegistryKeyPath(xml, INSTALLDIR, () -> propertyId, () -> { + // The following code converts a path to value to be saved in registry. + // E.g.: + // INSTALLDIR -> [INSTALLDIR] + // TERGETDIR/ProgramFiles64Folder/foo/bar -> [ProgramFiles64Folder]foo/bar + final Path rootDir = KNOWN_DIRS.stream() + .sorted(Comparator.comparing(Path::getNameCount).reversed()) + .filter(path::startsWith) + .findFirst().get(); + StringBuilder sb = new StringBuilder(); + sb.append(String.format("[%s]", rootDir.getFileName().toString())); + sb.append(rootDir.relativize(path).toString()); + return sb.toString(); + }); + + xml.writeStartElement( + "http://schemas.microsoft.com/wix/UtilExtension", + "RemoveFolderEx"); + xml.writeAttribute("On", "uninstall"); + xml.writeAttribute("Property", propertyId); + xml.writeEndElement(); // + xml.writeEndElement(); // + xml.writeEndElement(); // + + return componentId; + } + + private static IllegalArgumentException throwInvalidPathException(Path v) { + throw new IllegalArgumentException(String.format("Invalid path [%s]", v)); + } + + enum ShortcutsFolder { + ProgramMenu(PROGRAM_MENU_PATH), + Desktop(DESKTOP_PATH); + + private ShortcutsFolder(Path root) { + this.root = root; + } + + Path getPath(WixSourcesBuilder outer) { + if (this == ProgramMenu) { + return root.resolve(outer.programMenuFolderName); + } + return root; + } + + private final Path root; + } + + private DottedVersion wixVersion; + + private boolean systemWide; + + private String registryKeyPath; + + private Path installDir; + + private String programMenuFolderName; + + private List associations; + + private Set shortcutFolders; + + private List launcherPaths; + + private ApplicationLayout appImage; + private ApplicationLayout installedAppImage; + + private Map removeFolderItems; + private Set defaultedMimes; + + private final static Path TARGETDIR = Path.of("TARGETDIR"); + + private final static Path INSTALLDIR = Path.of("INSTALLDIR"); + + private final static Set ROOT_DIRS = Set.of(INSTALLDIR, TARGETDIR); + + private final static Path PROGRAM_MENU_PATH = TARGETDIR.resolve("ProgramMenuFolder"); + + private final static Path DESKTOP_PATH = TARGETDIR.resolve("DesktopFolder"); + + private final static Path PROGRAM_FILES = TARGETDIR.resolve("ProgramFiles64Folder"); + + private final static Path LOCAL_PROGRAM_FILES = TARGETDIR.resolve("LocalAppDataFolder"); + + private final static Set SYSTEM_DIRS = Set.of(TARGETDIR, + PROGRAM_MENU_PATH, DESKTOP_PATH, PROGRAM_FILES, LOCAL_PROGRAM_FILES); + + private final static Set KNOWN_DIRS = Stream.of(Set.of(INSTALLDIR), + SYSTEM_DIRS).flatMap(Set::stream).collect( + Collectors.toUnmodifiableSet()); + + private final static Set USER_PROFILE_DIRS = Set.of(LOCAL_PROGRAM_FILES, + PROGRAM_MENU_PATH, DESKTOP_PATH); + + private static final StandardBundlerParam MENU_HINT = + new WindowsBundlerParam<>( + Arguments.CLIOptions.WIN_MENU_HINT.getId(), + Boolean.class, + params -> false, + // valueOf(null) is false, + // and we actually do want null in some cases + (s, p) -> (s == null || + "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s) + ); + + private static final StandardBundlerParam SHORTCUT_HINT = + new WindowsBundlerParam<>( + Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(), + Boolean.class, + params -> false, + // valueOf(null) is false, + // and we actually do want null in some cases + (s, p) -> (s == null || + "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s) + ); +}