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