src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixSourcesBuilder.java
branchJDK-8200758-branch
changeset 58994 b09ba68c6a19
parent 58993 b5e1baa9d2c3
child 58995 de1413ae214c
equal deleted inserted replaced
58993:b5e1baa9d2c3 58994:b09ba68c6a19
     1 /*
       
     2  * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.  Oracle designates this
       
     8  * particular file as subject to the "Classpath" exception as provided
       
     9  * by Oracle in the LICENSE file that accompanied this code.
       
    10  *
       
    11  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    14  * version 2 for more details (a copy is included in the LICENSE file that
       
    15  * accompanied this code).
       
    16  *
       
    17  * You should have received a copy of the GNU General Public License version
       
    18  * 2 along with this work; if not, write to the Free Software Foundation,
       
    19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    20  *
       
    21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    22  * or visit www.oracle.com if you need additional information or have any
       
    23  * questions.
       
    24  */
       
    25 
       
    26 package jdk.jpackage.internal;
       
    27 
       
    28 import java.io.IOException;
       
    29 import java.nio.charset.StandardCharsets;
       
    30 import java.nio.file.Path;
       
    31 import java.text.MessageFormat;
       
    32 import java.util.*;
       
    33 import java.util.function.*;
       
    34 import java.util.stream.Collectors;
       
    35 import java.util.stream.Stream;
       
    36 import javax.xml.stream.XMLStreamException;
       
    37 import javax.xml.stream.XMLStreamWriter;
       
    38 import jdk.jpackage.internal.IOUtils.XmlConsumer;
       
    39 import static jdk.jpackage.internal.StandardBundlerParam.*;
       
    40 import static jdk.jpackage.internal.WinMsiBundler.*;
       
    41 import static jdk.jpackage.internal.WindowsBundlerParam.MENU_GROUP;
       
    42 import static jdk.jpackage.internal.WindowsBundlerParam.WINDOWS_INSTALL_DIR;
       
    43 
       
    44 /**
       
    45  * Creates application WiX source files.
       
    46  */
       
    47 class WixSourcesBuilder {
       
    48 
       
    49     WixSourcesBuilder setWixVersion(DottedVersion v) {
       
    50         wixVersion = v;
       
    51         return this;
       
    52     }
       
    53 
       
    54     WixSourcesBuilder initFromParams(Path appImageRoot,
       
    55             Map<String, ? super Object> params) {
       
    56         Supplier<ApplicationLayout> appImageSupplier = () -> {
       
    57             if (StandardBundlerParam.isRuntimeInstaller(params)) {
       
    58                 return ApplicationLayout.javaRuntime();
       
    59             } else {
       
    60                 return ApplicationLayout.platformAppImage();
       
    61             }
       
    62         };
       
    63 
       
    64         systemWide = MSI_SYSTEM_WIDE.fetchFrom(params);
       
    65 
       
    66         registryKeyPath = Path.of("Software",
       
    67                 VENDOR.fetchFrom(params),
       
    68                 APP_NAME.fetchFrom(params),
       
    69                 VERSION.fetchFrom(params)).toString();
       
    70 
       
    71         installDir = (systemWide ? PROGRAM_FILES : LOCAL_PROGRAM_FILES).resolve(
       
    72                 WINDOWS_INSTALL_DIR.fetchFrom(params));
       
    73 
       
    74         do {
       
    75             ApplicationLayout layout = appImageSupplier.get();
       
    76             // Don't want AppImageFile.FILENAME in installed application.
       
    77             // Register it with app image at a role without a match in installed
       
    78             // app layout to exclude it from layout transformation.
       
    79             layout.pathGroup().setPath(new Object(),
       
    80                     AppImageFile.getPathInAppImage(Path.of("")));
       
    81 
       
    82             // Want absolute paths to source files in generated WiX sources.
       
    83             // This is to handle scenario if sources would be processed from
       
    84             // differnt current directory.
       
    85             appImage = layout.resolveAt(appImageRoot.toAbsolutePath().normalize());
       
    86         } while (false);
       
    87 
       
    88         installedAppImage = appImageSupplier.get().resolveAt(INSTALLDIR);
       
    89 
       
    90         shortcutFolders = new HashSet<>();
       
    91         if (SHORTCUT_HINT.fetchFrom(params)) {
       
    92             shortcutFolders.add(ShortcutsFolder.Desktop);
       
    93         }
       
    94         if (MENU_HINT.fetchFrom(params)) {
       
    95             shortcutFolders.add(ShortcutsFolder.ProgramMenu);
       
    96         }
       
    97 
       
    98         if (StandardBundlerParam.isRuntimeInstaller(params)) {
       
    99             launcherPaths = Collections.emptyList();
       
   100         } else {
       
   101             launcherPaths = AppImageFile.getLauncherNames(appImageRoot, params).stream()
       
   102                     .map(name -> installedAppImage.launchersDirectory().resolve(name))
       
   103                     .map(WixSourcesBuilder::addExeSuffixToPath)
       
   104                     .collect(Collectors.toList());
       
   105         }
       
   106 
       
   107         programMenuFolderName = MENU_GROUP.fetchFrom(params);
       
   108 
       
   109         initFileAssociations(params);
       
   110 
       
   111         return this;
       
   112     }
       
   113 
       
   114     void createMainFragment(Path file) throws IOException {
       
   115         removeFolderItems = new HashMap<>();
       
   116         defaultedMimes = new HashSet<>();
       
   117         IOUtils.createXml(file, xml -> {
       
   118             xml.writeStartElement("Wix");
       
   119             xml.writeDefaultNamespace("http://schemas.microsoft.com/wix/2006/wi");
       
   120             xml.writeNamespace("util",
       
   121                     "http://schemas.microsoft.com/wix/UtilExtension");
       
   122 
       
   123             xml.writeStartElement("Fragment");
       
   124 
       
   125             addFaComponentGroup(xml);
       
   126 
       
   127             addShortcutComponentGroup(xml);
       
   128 
       
   129             addFilesComponentGroup(xml);
       
   130 
       
   131             xml.writeEndElement();  // <Fragment>
       
   132 
       
   133             addIconsFragment(xml);
       
   134 
       
   135             xml.writeEndElement(); // <Wix>
       
   136         });
       
   137     }
       
   138 
       
   139     void logWixFeatures() {
       
   140         if (wixVersion.compareTo("3.6") >= 0) {
       
   141             Log.verbose(MessageFormat.format(I18N.getString(
       
   142                     "message.use-wix36-features"), wixVersion));
       
   143         }
       
   144     }
       
   145 
       
   146     private void normalizeFileAssociation(FileAssociation fa) {
       
   147         fa.launcherPath = addExeSuffixToPath(
       
   148                 installedAppImage.launchersDirectory().resolve(fa.launcherPath));
       
   149 
       
   150         if (fa.iconPath != null && !fa.iconPath.toFile().exists()) {
       
   151             fa.iconPath = null;
       
   152         }
       
   153 
       
   154         if (fa.iconPath != null) {
       
   155             fa.iconPath = fa.iconPath.toAbsolutePath();
       
   156         }
       
   157 
       
   158         // Filter out empty extensions.
       
   159         fa.extensions = fa.extensions.stream().filter(Predicate.not(
       
   160                 String::isEmpty)).collect(Collectors.toList());
       
   161     }
       
   162 
       
   163     private static Path addExeSuffixToPath(Path path) {
       
   164         return IOUtils.addSuffix(path, ".exe");
       
   165     }
       
   166 
       
   167     private Path getInstalledFaIcoPath(FileAssociation fa) {
       
   168         String fname = String.format("fa_%s.ico", String.join("_", fa.extensions));
       
   169         return installedAppImage.destktopIntegrationDirectory().resolve(fname);
       
   170     }
       
   171 
       
   172     private void initFileAssociations(Map<String, ? super Object> params) {
       
   173         associations = FileAssociation.fetchFrom(params).stream()
       
   174                 .peek(this::normalizeFileAssociation)
       
   175                 // Filter out file associations without extensions.
       
   176                 .filter(fa -> !fa.extensions.isEmpty())
       
   177                 .collect(Collectors.toList());
       
   178 
       
   179         associations.stream().filter(fa -> fa.iconPath != null).forEach(fa -> {
       
   180             // Need to add fa icon in the image.
       
   181             Object key = new Object();
       
   182             appImage.pathGroup().setPath(key, fa.iconPath);
       
   183             installedAppImage.pathGroup().setPath(key, getInstalledFaIcoPath(fa));
       
   184         });
       
   185     }
       
   186 
       
   187     private static UUID createNameUUID(String str) {
       
   188         return UUID.nameUUIDFromBytes(str.getBytes(StandardCharsets.UTF_8));
       
   189     }
       
   190 
       
   191     private static UUID createNameUUID(Path path, String role) {
       
   192         if (path.isAbsolute() || !ROOT_DIRS.contains(path.getName(0))) {
       
   193             throw throwInvalidPathException(path);
       
   194         }
       
   195         // Paths are case insensitive on Windows
       
   196         String keyPath = path.toString().toLowerCase();
       
   197         if (role != null) {
       
   198             keyPath = role + "@" + keyPath;
       
   199         }
       
   200         return createNameUUID(keyPath);
       
   201     }
       
   202 
       
   203     /**
       
   204      * Value for Id attribute of various WiX elements.
       
   205      */
       
   206     enum Id {
       
   207         File,
       
   208         Folder("dir"),
       
   209         Shortcut,
       
   210         ProgId,
       
   211         Icon,
       
   212         CreateFolder("mkdir"),
       
   213         RemoveFolder("rm");
       
   214 
       
   215         Id() {
       
   216             this.prefix = name().toLowerCase();
       
   217         }
       
   218 
       
   219         Id(String prefix) {
       
   220             this.prefix = prefix;
       
   221         }
       
   222 
       
   223         String of(Path path) {
       
   224             if (this == Folder && KNOWN_DIRS.contains(path)) {
       
   225                 return path.getFileName().toString();
       
   226             }
       
   227 
       
   228             String result = of(path, prefix, name());
       
   229 
       
   230             if (this == Icon) {
       
   231                 // Icon id constructed from UUID value is too long and triggers
       
   232                 // CNDL1000 warning, so use Java hash code instead.
       
   233                 result = String.format("%s%d", prefix, result.hashCode()).replace(
       
   234                         "-", "_");
       
   235             }
       
   236 
       
   237             return result;
       
   238         }
       
   239 
       
   240         private static String of(Path path, String prefix, String role) {
       
   241             Objects.requireNonNull(role);
       
   242             Objects.requireNonNull(prefix);
       
   243             return String.format("%s%s", prefix,
       
   244                     createNameUUID(path, role).toString().replace("-", ""));
       
   245         }
       
   246 
       
   247         static String of(Path path, String prefix) {
       
   248             return of(path, prefix, prefix);
       
   249         }
       
   250 
       
   251         private final String prefix;
       
   252     }
       
   253 
       
   254     enum Component {
       
   255         File(cfg().file()),
       
   256         Shortcut(cfg().file().withRegistryKeyPath()),
       
   257         ProgId(cfg().file().withRegistryKeyPath()),
       
   258         CreateFolder(cfg().withRegistryKeyPath()),
       
   259         RemoveFolder(cfg().withRegistryKeyPath());
       
   260 
       
   261         Component() {
       
   262             this.cfg = cfg();
       
   263             this.id = Id.valueOf(name());
       
   264         }
       
   265 
       
   266         Component(Config cfg) {
       
   267             this.cfg = cfg;
       
   268             this.id = Id.valueOf(name());
       
   269         }
       
   270 
       
   271         UUID guidOf(Path path) {
       
   272             return createNameUUID(path, name());
       
   273         }
       
   274 
       
   275         String idOf(Path path) {
       
   276             return id.of(path);
       
   277         }
       
   278 
       
   279         boolean isRegistryKeyPath() {
       
   280             return cfg.withRegistryKeyPath;
       
   281         }
       
   282 
       
   283         boolean isFile() {
       
   284             return cfg.isFile;
       
   285         }
       
   286 
       
   287         static void startElement(XMLStreamWriter xml, String componentId,
       
   288                 String componentGuid) throws XMLStreamException, IOException {
       
   289             xml.writeStartElement("Component");
       
   290             xml.writeAttribute("Win64", "yes");
       
   291             xml.writeAttribute("Id", componentId);
       
   292             xml.writeAttribute("Guid", componentGuid);
       
   293         }
       
   294 
       
   295         private static final class Config {
       
   296             Config withRegistryKeyPath() {
       
   297                 withRegistryKeyPath = true;
       
   298                 return this;
       
   299             }
       
   300 
       
   301             Config file() {
       
   302                 isFile = true;
       
   303                 return this;
       
   304             }
       
   305 
       
   306             private boolean isFile;
       
   307             private boolean withRegistryKeyPath;
       
   308         }
       
   309 
       
   310         private static Config cfg() {
       
   311             return new Config();
       
   312         }
       
   313 
       
   314         private final Config cfg;
       
   315         private final Id id;
       
   316     };
       
   317 
       
   318     private static void addComponentGroup(XMLStreamWriter xml, String id,
       
   319             List<String> componentIds) throws XMLStreamException, IOException {
       
   320         xml.writeStartElement("ComponentGroup");
       
   321         xml.writeAttribute("Id", id);
       
   322         componentIds = componentIds.stream().filter(Objects::nonNull).collect(
       
   323                 Collectors.toList());
       
   324         for (var componentId : componentIds) {
       
   325             xml.writeStartElement("ComponentRef");
       
   326             xml.writeAttribute("Id", componentId);
       
   327             xml.writeEndElement();
       
   328         }
       
   329         xml.writeEndElement();
       
   330     }
       
   331 
       
   332     private String addComponent(XMLStreamWriter xml, Path path,
       
   333             Component role, XmlConsumer xmlConsumer) throws XMLStreamException,
       
   334             IOException {
       
   335 
       
   336         final Path directoryRefPath;
       
   337         if (role.isFile()) {
       
   338             directoryRefPath = path.getParent();
       
   339         } else {
       
   340             directoryRefPath = path;
       
   341         }
       
   342 
       
   343         xml.writeStartElement("DirectoryRef");
       
   344         xml.writeAttribute("Id", Id.Folder.of(directoryRefPath));
       
   345 
       
   346         final String componentId = "c" + role.idOf(path);
       
   347         Component.startElement(xml, componentId, String.format("{%s}",
       
   348                 role.guidOf(path)));
       
   349 
       
   350         boolean isRegistryKeyPath = !systemWide || role.isRegistryKeyPath();
       
   351         if (isRegistryKeyPath) {
       
   352             addRegistryKeyPath(xml, directoryRefPath);
       
   353             if ((role.isFile() || (role == Component.CreateFolder
       
   354                     && !systemWide)) && !SYSTEM_DIRS.contains(directoryRefPath)) {
       
   355                 xml.writeStartElement("RemoveFolder");
       
   356                 int counter = Optional.ofNullable(removeFolderItems.get(
       
   357                         directoryRefPath)).orElse(Integer.valueOf(0)).intValue() + 1;
       
   358                 removeFolderItems.put(directoryRefPath, counter);
       
   359                 xml.writeAttribute("Id", String.format("%s_%d", Id.RemoveFolder.of(
       
   360                         directoryRefPath), counter));
       
   361                 xml.writeAttribute("On", "uninstall");
       
   362                 xml.writeEndElement();
       
   363             }
       
   364         }
       
   365 
       
   366         xml.writeStartElement(role.name());
       
   367         if (role != Component.CreateFolder) {
       
   368             xml.writeAttribute("Id", role.idOf(path));
       
   369         }
       
   370 
       
   371         if (!isRegistryKeyPath) {
       
   372             xml.writeAttribute("KeyPath", "yes");
       
   373         }
       
   374 
       
   375         xmlConsumer.accept(xml);
       
   376         xml.writeEndElement();
       
   377 
       
   378         xml.writeEndElement(); // <Component>
       
   379         xml.writeEndElement(); // <DirectoryRef>
       
   380 
       
   381         return componentId;
       
   382     }
       
   383 
       
   384     private void addFaComponentGroup(XMLStreamWriter xml)
       
   385             throws XMLStreamException, IOException {
       
   386 
       
   387         List<String> componentIds = new ArrayList<>();
       
   388         for (var fa : associations) {
       
   389             componentIds.addAll(addFaComponents(xml, fa));
       
   390         }
       
   391         addComponentGroup(xml, "FileAssociations", componentIds);
       
   392     }
       
   393 
       
   394     private void addShortcutComponentGroup(XMLStreamWriter xml) throws
       
   395             XMLStreamException, IOException {
       
   396         List<String> componentIds = new ArrayList<>();
       
   397         Set<ShortcutsFolder> defineShortcutFolders = new HashSet<>();
       
   398         for (var launcherPath : launcherPaths) {
       
   399             for (var folder : shortcutFolders) {
       
   400                 String componentId = addShortcutComponent(xml, launcherPath,
       
   401                         folder);
       
   402                 if (componentId != null) {
       
   403                     defineShortcutFolders.add(folder);
       
   404                     componentIds.add(componentId);
       
   405                 }
       
   406             }
       
   407         }
       
   408 
       
   409         for (var folder : defineShortcutFolders) {
       
   410             Path path = folder.getPath(this);
       
   411             componentIds.addAll(addRootBranch(xml, path));
       
   412         }
       
   413 
       
   414         addComponentGroup(xml, "Shortcuts", componentIds);
       
   415     }
       
   416 
       
   417     private String addShortcutComponent(XMLStreamWriter xml, Path launcherPath,
       
   418             ShortcutsFolder folder) throws XMLStreamException, IOException {
       
   419         Objects.requireNonNull(folder);
       
   420 
       
   421         if (!INSTALLDIR.equals(launcherPath.getName(0))) {
       
   422             throw throwInvalidPathException(launcherPath);
       
   423         }
       
   424 
       
   425         String launcherBasename = IOUtils.replaceSuffix(
       
   426                 launcherPath.getFileName(), "").toString();
       
   427 
       
   428         Path shortcutPath = folder.getPath(this).resolve(launcherBasename);
       
   429         return addComponent(xml, shortcutPath, Component.Shortcut, unused -> {
       
   430             final Path icoFile = IOUtils.addSuffix(
       
   431                     installedAppImage.destktopIntegrationDirectory().resolve(
       
   432                             launcherBasename), ".ico");
       
   433 
       
   434             xml.writeAttribute("Name", launcherBasename);
       
   435             xml.writeAttribute("WorkingDirectory", INSTALLDIR.toString());
       
   436             xml.writeAttribute("Advertise", "no");
       
   437             xml.writeAttribute("IconIndex", "0");
       
   438             xml.writeAttribute("Target", String.format("[#%s]",
       
   439                     Component.File.idOf(launcherPath)));
       
   440             xml.writeAttribute("Icon", Id.Icon.of(icoFile));
       
   441         });
       
   442     }
       
   443 
       
   444     private List<String> addFaComponents(XMLStreamWriter xml,
       
   445             FileAssociation fa) throws XMLStreamException, IOException {
       
   446         List<String> components = new ArrayList<>();
       
   447         for (var extension: fa.extensions) {
       
   448             Path path = INSTALLDIR.resolve(String.format("%s_%s", extension,
       
   449                     fa.launcherPath.getFileName()));
       
   450             components.add(addComponent(xml, path, Component.ProgId, unused -> {
       
   451                 xml.writeAttribute("Description", fa.description);
       
   452 
       
   453                 if (fa.iconPath != null) {
       
   454                     xml.writeAttribute("Icon", Id.File.of(getInstalledFaIcoPath(
       
   455                             fa)));
       
   456                     xml.writeAttribute("IconIndex", "0");
       
   457                 }
       
   458 
       
   459                 xml.writeStartElement("Extension");
       
   460                 xml.writeAttribute("Id", extension);
       
   461                 xml.writeAttribute("Advertise", "no");
       
   462 
       
   463                 var mimeIt = fa.mimeTypes.iterator();
       
   464                 if (mimeIt.hasNext()) {
       
   465                     String mime = mimeIt.next();
       
   466                     xml.writeAttribute("ContentType", mime);
       
   467 
       
   468                     if (!defaultedMimes.contains(mime)) {
       
   469                         xml.writeStartElement("MIME");
       
   470                         xml.writeAttribute("ContentType", mime);
       
   471                         xml.writeAttribute("Default", "yes");
       
   472                         xml.writeEndElement();
       
   473                         defaultedMimes.add(mime);
       
   474                     }
       
   475                 }
       
   476 
       
   477                 xml.writeStartElement("Verb");
       
   478                 xml.writeAttribute("Id", "open");
       
   479                 xml.writeAttribute("Command", "Open");
       
   480                 xml.writeAttribute("Argument", "%1");
       
   481                 xml.writeAttribute("TargetFile", Id.File.of(fa.launcherPath));
       
   482                 xml.writeEndElement(); // <Verb>
       
   483 
       
   484                 xml.writeEndElement(); // <Extension>
       
   485             }));
       
   486         }
       
   487 
       
   488         return components;
       
   489     }
       
   490 
       
   491     private List<String> addRootBranch(XMLStreamWriter xml, Path path)
       
   492             throws XMLStreamException, IOException {
       
   493         if (!ROOT_DIRS.contains(path.getName(0))) {
       
   494             throw throwInvalidPathException(path);
       
   495         }
       
   496 
       
   497         Function<Path, String> createDirectoryName = dir -> null;
       
   498 
       
   499         boolean sysDir = true;
       
   500         int levels = 1;
       
   501         var dirIt = path.iterator();
       
   502         xml.writeStartElement("DirectoryRef");
       
   503         xml.writeAttribute("Id", dirIt.next().toString());
       
   504 
       
   505         path = path.getName(0);
       
   506         while (dirIt.hasNext()) {
       
   507             levels++;
       
   508             Path name = dirIt.next();
       
   509             path = path.resolve(name);
       
   510 
       
   511             if (sysDir && !SYSTEM_DIRS.contains(path)) {
       
   512                 sysDir = false;
       
   513                 createDirectoryName = dir -> dir.getFileName().toString();
       
   514             }
       
   515 
       
   516             final String directoryId;
       
   517             if (!sysDir && path.equals(installDir)) {
       
   518                 directoryId = INSTALLDIR.toString();
       
   519             } else {
       
   520                 directoryId = Id.Folder.of(path);
       
   521             }
       
   522             xml.writeStartElement("Directory");
       
   523             xml.writeAttribute("Id", directoryId);
       
   524 
       
   525             String directoryName = createDirectoryName.apply(path);
       
   526             if (directoryName != null) {
       
   527                 xml.writeAttribute("Name", directoryName);
       
   528             }
       
   529         }
       
   530 
       
   531         while (0 != levels--) {
       
   532             xml.writeEndElement();
       
   533         }
       
   534 
       
   535         List<String> componentIds = new ArrayList<>();
       
   536         while (!SYSTEM_DIRS.contains(path = path.getParent())) {
       
   537             componentIds.add(addRemoveDirectoryComponent(xml, path));
       
   538         }
       
   539 
       
   540         return componentIds;
       
   541     }
       
   542 
       
   543     private String addRemoveDirectoryComponent(XMLStreamWriter xml, Path path)
       
   544             throws XMLStreamException, IOException {
       
   545         return addComponent(xml, path, Component.RemoveFolder,
       
   546                 unused -> xml.writeAttribute("On", "uninstall"));
       
   547     }
       
   548 
       
   549     private List<String> addDirectoryHierarchy(XMLStreamWriter xml)
       
   550             throws XMLStreamException, IOException {
       
   551 
       
   552         Set<Path> allDirs = new HashSet<>();
       
   553         Set<Path> emptyDirs = new HashSet<>();
       
   554         appImage.transform(installedAppImage, new PathGroup.TransformHandler() {
       
   555             @Override
       
   556             public void copyFile(Path src, Path dst) throws IOException {
       
   557                 Path dir = dst.getParent();
       
   558                 createDirectory(dir);
       
   559                 emptyDirs.remove(dir);
       
   560             }
       
   561 
       
   562             @Override
       
   563             public void createDirectory(final Path dir) throws IOException {
       
   564                 if (!allDirs.contains(dir)) {
       
   565                     emptyDirs.add(dir);
       
   566                 }
       
   567 
       
   568                 Path it = dir;
       
   569                 while (it != null && allDirs.add(it)) {
       
   570                     it = it.getParent();
       
   571                 }
       
   572 
       
   573                 it = dir;
       
   574                 while ((it = it.getParent()) != null && emptyDirs.remove(it));
       
   575             }
       
   576         });
       
   577 
       
   578         List<String> componentIds = new ArrayList<>();
       
   579         for (var dir : emptyDirs) {
       
   580             componentIds.add(addComponent(xml, dir, Component.CreateFolder,
       
   581                     unused -> {}));
       
   582         }
       
   583 
       
   584         if (!systemWide) {
       
   585             // Per-user install requires <RemoveFolder> component in every
       
   586             // directory.
       
   587             for (var dir : allDirs.stream()
       
   588                     .filter(Predicate.not(emptyDirs::contains))
       
   589                     .filter(Predicate.not(removeFolderItems::containsKey))
       
   590                     .collect(Collectors.toList())) {
       
   591                 componentIds.add(addRemoveDirectoryComponent(xml, dir));
       
   592             }
       
   593         }
       
   594 
       
   595         allDirs.remove(INSTALLDIR);
       
   596         for (var dir : allDirs) {
       
   597             xml.writeStartElement("DirectoryRef");
       
   598             xml.writeAttribute("Id", Id.Folder.of(dir.getParent()));
       
   599             xml.writeStartElement("Directory");
       
   600             xml.writeAttribute("Id", Id.Folder.of(dir));
       
   601             xml.writeAttribute("Name", dir.getFileName().toString());
       
   602             xml.writeEndElement();
       
   603             xml.writeEndElement();
       
   604         }
       
   605 
       
   606         componentIds.addAll(addRootBranch(xml, installDir));
       
   607 
       
   608         return componentIds;
       
   609     }
       
   610 
       
   611     private void addFilesComponentGroup(XMLStreamWriter xml)
       
   612             throws XMLStreamException, IOException {
       
   613 
       
   614         List<Map.Entry<Path, Path>> files = new ArrayList<>();
       
   615         appImage.transform(installedAppImage, new PathGroup.TransformHandler() {
       
   616             @Override
       
   617             public void copyFile(Path src, Path dst) throws IOException {
       
   618                 files.add(Map.entry(src, dst));
       
   619             }
       
   620 
       
   621             @Override
       
   622             public void createDirectory(final Path dir) throws IOException {
       
   623             }
       
   624         });
       
   625 
       
   626         List<String> componentIds = new ArrayList<>();
       
   627         for (var file : files) {
       
   628             Path src = file.getKey();
       
   629             Path dst = file.getValue();
       
   630 
       
   631             componentIds.add(addComponent(xml, dst, Component.File, unused -> {
       
   632                 xml.writeAttribute("Source", src.normalize().toString());
       
   633                 Path name = dst.getFileName();
       
   634                 if (!name.equals(src.getFileName())) {
       
   635                     xml.writeAttribute("Name", name.toString());
       
   636                 }
       
   637             }));
       
   638         }
       
   639 
       
   640         componentIds.addAll(addDirectoryHierarchy(xml));
       
   641 
       
   642         componentIds.add(addDirectoryCleaner(xml, INSTALLDIR));
       
   643 
       
   644         addComponentGroup(xml, "Files", componentIds);
       
   645     }
       
   646 
       
   647     private void addIconsFragment(XMLStreamWriter xml) throws
       
   648             XMLStreamException, IOException {
       
   649 
       
   650         PathGroup srcPathGroup = appImage.pathGroup();
       
   651         PathGroup dstPathGroup = installedAppImage.pathGroup();
       
   652 
       
   653         // Build list of copy operations for all .ico files in application image
       
   654         List<Map.Entry<Path, Path>> icoFiles = new ArrayList<>();
       
   655         srcPathGroup.transform(dstPathGroup, new PathGroup.TransformHandler() {
       
   656             @Override
       
   657             public void copyFile(Path src, Path dst) throws IOException {
       
   658                 if (src.getFileName().toString().endsWith(".ico")) {
       
   659                     icoFiles.add(Map.entry(src, dst));
       
   660                 }
       
   661             }
       
   662 
       
   663             @Override
       
   664             public void createDirectory(Path dst) throws IOException {
       
   665             }
       
   666         });
       
   667 
       
   668         xml.writeStartElement("Fragment");
       
   669         for (var icoFile : icoFiles) {
       
   670             xml.writeStartElement("Icon");
       
   671             xml.writeAttribute("Id", Id.Icon.of(icoFile.getValue()));
       
   672             xml.writeAttribute("SourceFile", icoFile.getKey().toString());
       
   673             xml.writeEndElement();
       
   674         }
       
   675         xml.writeEndElement();
       
   676     }
       
   677 
       
   678     private void addRegistryKeyPath(XMLStreamWriter xml, Path path) throws
       
   679             XMLStreamException, IOException {
       
   680         addRegistryKeyPath(xml, path, () -> "ProductCode", () -> "[ProductCode]");
       
   681     }
       
   682 
       
   683     private void addRegistryKeyPath(XMLStreamWriter xml, Path path,
       
   684             Supplier<String> nameAttr, Supplier<String> valueAttr) throws
       
   685             XMLStreamException, IOException {
       
   686 
       
   687         String regRoot = USER_PROFILE_DIRS.stream().anyMatch(path::startsWith)
       
   688                 || !systemWide ? "HKCU" : "HKLM";
       
   689 
       
   690         xml.writeStartElement("RegistryKey");
       
   691         xml.writeAttribute("Root", regRoot);
       
   692         xml.writeAttribute("Key", registryKeyPath);
       
   693         if (wixVersion.compareTo("3.6") < 0) {
       
   694             xml.writeAttribute("Action", "createAndRemoveOnUninstall");
       
   695         }
       
   696         xml.writeStartElement("RegistryValue");
       
   697         xml.writeAttribute("Type", "string");
       
   698         xml.writeAttribute("KeyPath", "yes");
       
   699         xml.writeAttribute("Name", nameAttr.get());
       
   700         xml.writeAttribute("Value", valueAttr.get());
       
   701         xml.writeEndElement(); // <RegistryValue>
       
   702         xml.writeEndElement(); // <RegistryKey>
       
   703     }
       
   704 
       
   705     private String addDirectoryCleaner(XMLStreamWriter xml, Path path) throws
       
   706             XMLStreamException, IOException {
       
   707         if (wixVersion.compareTo("3.6") < 0) {
       
   708             return null;
       
   709         }
       
   710 
       
   711         // rm -rf
       
   712         final String baseId = Id.of(path, "rm_rf");
       
   713         final String propertyId = baseId.toUpperCase();
       
   714         final String componentId = ("c" + baseId);
       
   715 
       
   716         xml.writeStartElement("Property");
       
   717         xml.writeAttribute("Id", propertyId);
       
   718         xml.writeStartElement("RegistrySearch");
       
   719         xml.writeAttribute("Id", Id.of(path, "regsearch"));
       
   720         xml.writeAttribute("Root", systemWide ? "HKLM" : "HKCU");
       
   721         xml.writeAttribute("Key", registryKeyPath);
       
   722         xml.writeAttribute("Type", "raw");
       
   723         xml.writeAttribute("Name", propertyId);
       
   724         xml.writeEndElement(); // <RegistrySearch>
       
   725         xml.writeEndElement(); // <Property>
       
   726 
       
   727         xml.writeStartElement("DirectoryRef");
       
   728         xml.writeAttribute("Id", INSTALLDIR.toString());
       
   729         Component.startElement(xml, componentId, "*");
       
   730 
       
   731         addRegistryKeyPath(xml, INSTALLDIR, () -> propertyId, () -> {
       
   732             // The following code converts a path to value to be saved in registry.
       
   733             // E.g.:
       
   734             //  INSTALLDIR -> [INSTALLDIR]
       
   735             //  TERGETDIR/ProgramFiles64Folder/foo/bar -> [ProgramFiles64Folder]foo/bar
       
   736             final Path rootDir = KNOWN_DIRS.stream()
       
   737                     .sorted(Comparator.comparing(Path::getNameCount).reversed())
       
   738                     .filter(path::startsWith)
       
   739                     .findFirst().get();
       
   740             StringBuilder sb = new StringBuilder();
       
   741             sb.append(String.format("[%s]", rootDir.getFileName().toString()));
       
   742             sb.append(rootDir.relativize(path).toString());
       
   743             return sb.toString();
       
   744         });
       
   745 
       
   746         xml.writeStartElement(
       
   747                 "http://schemas.microsoft.com/wix/UtilExtension",
       
   748                 "RemoveFolderEx");
       
   749         xml.writeAttribute("On", "uninstall");
       
   750         xml.writeAttribute("Property", propertyId);
       
   751         xml.writeEndElement(); // <RemoveFolderEx>
       
   752         xml.writeEndElement(); // <Component>
       
   753         xml.writeEndElement(); // <DirectoryRef>
       
   754 
       
   755         return componentId;
       
   756     }
       
   757 
       
   758     private static IllegalArgumentException throwInvalidPathException(Path v) {
       
   759         throw new IllegalArgumentException(String.format("Invalid path [%s]", v));
       
   760     }
       
   761 
       
   762     enum ShortcutsFolder {
       
   763         ProgramMenu(PROGRAM_MENU_PATH),
       
   764         Desktop(DESKTOP_PATH);
       
   765 
       
   766         private ShortcutsFolder(Path root) {
       
   767             this.root = root;
       
   768         }
       
   769 
       
   770         Path getPath(WixSourcesBuilder outer) {
       
   771             if (this == ProgramMenu) {
       
   772                 return root.resolve(outer.programMenuFolderName);
       
   773             }
       
   774             return root;
       
   775         }
       
   776 
       
   777         private final Path root;
       
   778     }
       
   779 
       
   780     private DottedVersion wixVersion;
       
   781 
       
   782     private boolean systemWide;
       
   783 
       
   784     private String registryKeyPath;
       
   785 
       
   786     private Path installDir;
       
   787 
       
   788     private String programMenuFolderName;
       
   789 
       
   790     private List<FileAssociation> associations;
       
   791 
       
   792     private Set<ShortcutsFolder> shortcutFolders;
       
   793 
       
   794     private List<Path> launcherPaths;
       
   795 
       
   796     private ApplicationLayout appImage;
       
   797     private ApplicationLayout installedAppImage;
       
   798 
       
   799     private Map<Path, Integer> removeFolderItems;
       
   800     private Set<String> defaultedMimes;
       
   801 
       
   802     private final static Path TARGETDIR = Path.of("TARGETDIR");
       
   803 
       
   804     private final static Path INSTALLDIR = Path.of("INSTALLDIR");
       
   805 
       
   806     private final static Set<Path> ROOT_DIRS = Set.of(INSTALLDIR, TARGETDIR);
       
   807 
       
   808     private final static Path PROGRAM_MENU_PATH = TARGETDIR.resolve("ProgramMenuFolder");
       
   809 
       
   810     private final static Path DESKTOP_PATH = TARGETDIR.resolve("DesktopFolder");
       
   811 
       
   812     private final static Path PROGRAM_FILES = TARGETDIR.resolve("ProgramFiles64Folder");
       
   813 
       
   814     private final static Path LOCAL_PROGRAM_FILES = TARGETDIR.resolve("LocalAppDataFolder");
       
   815 
       
   816     private final static Set<Path> SYSTEM_DIRS = Set.of(TARGETDIR,
       
   817             PROGRAM_MENU_PATH, DESKTOP_PATH, PROGRAM_FILES, LOCAL_PROGRAM_FILES);
       
   818 
       
   819     private final static Set<Path> KNOWN_DIRS = Stream.of(Set.of(INSTALLDIR),
       
   820             SYSTEM_DIRS).flatMap(Set::stream).collect(
       
   821             Collectors.toUnmodifiableSet());
       
   822 
       
   823     private final static Set<Path> USER_PROFILE_DIRS = Set.of(LOCAL_PROGRAM_FILES,
       
   824             PROGRAM_MENU_PATH, DESKTOP_PATH);
       
   825 
       
   826     private static final StandardBundlerParam<Boolean> MENU_HINT =
       
   827         new WindowsBundlerParam<>(
       
   828                 Arguments.CLIOptions.WIN_MENU_HINT.getId(),
       
   829                 Boolean.class,
       
   830                 params -> false,
       
   831                 // valueOf(null) is false,
       
   832                 // and we actually do want null in some cases
       
   833                 (s, p) -> (s == null ||
       
   834                         "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
       
   835         );
       
   836 
       
   837     private static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
       
   838         new WindowsBundlerParam<>(
       
   839                 Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(),
       
   840                 Boolean.class,
       
   841                 params -> false,
       
   842                 // valueOf(null) is false,
       
   843                 // and we actually do want null in some cases
       
   844                 (s, p) -> (s == null ||
       
   845                        "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
       
   846         );
       
   847 }