1 /*
   2  * Copyright (c) 2012, 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.*;
  29 import java.nio.charset.Charset;
  30 import java.nio.file.Files;
  31 import java.text.MessageFormat;
  32 import java.util.*;
  33 import java.util.regex.Pattern;
  34 
  35 import static jdk.jpackage.internal.WindowsBundlerParam.*;
  36 
  37 public class WinMsiBundler  extends AbstractBundler {
  38 
  39     private static final ResourceBundle I18N = ResourceBundle.getBundle(
  40             "jdk.jpackage.internal.resources.WinResources");
  41 
  42     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER =
  43             new WindowsBundlerParam<>(
  44             "win.app.bundler",
  45             WinAppBundler.class,
  46             params -> new WinAppBundler(),
  47             null);
  48 
  49     public static final BundlerParamInfo<Boolean> CAN_USE_WIX36 =
  50             new WindowsBundlerParam<>(
  51             "win.msi.canUseWix36",
  52             Boolean.class,
  53             params -> false,
  54             (s, p) -> Boolean.valueOf(s));
  55 
  56     public static final BundlerParamInfo<File> MSI_IMAGE_DIR =
  57             new WindowsBundlerParam<>(
  58             "win.msi.imageDir",
  59             File.class,
  60             params -> {
  61                 File imagesRoot = IMAGES_ROOT.fetchFrom(params);
  62                 if (!imagesRoot.exists()) imagesRoot.mkdirs();
  63                 return new File(imagesRoot, "win-msi.image");
  64             },
  65             (s, p) -> null);
  66 
  67     public static final BundlerParamInfo<File> WIN_APP_IMAGE =
  68             new WindowsBundlerParam<>(
  69             "win.app.image",
  70             File.class,
  71             null,
  72             (s, p) -> null);
  73 
  74     public static final StandardBundlerParam<Boolean> MSI_SYSTEM_WIDE  =
  75             new StandardBundlerParam<>(
  76                     Arguments.CLIOptions.WIN_PER_USER_INSTALLATION.getId(),
  77                     Boolean.class,
  78                     params -> true, // MSIs default to system wide
  79                     // valueOf(null) is false,
  80                     // and we actually do want null
  81                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null
  82                             : Boolean.valueOf(s)
  83             );
  84 
  85 
  86     public static final StandardBundlerParam<String> PRODUCT_VERSION =
  87             new StandardBundlerParam<>(
  88                     "win.msi.productVersion",
  89                     String.class,
  90                     VERSION::fetchFrom,
  91                     (s, p) -> s
  92             );
  93 
  94     public static final BundlerParamInfo<UUID> UPGRADE_UUID =
  95             new WindowsBundlerParam<>(
  96             Arguments.CLIOptions.WIN_UPGRADE_UUID.getId(),
  97             UUID.class,
  98             params -> UUID.randomUUID(),
  99             (s, p) -> UUID.fromString(s));
 100 
 101     private static final String TOOL_CANDLE = "candle.exe";
 102     private static final String TOOL_LIGHT = "light.exe";
 103     // autodetect just v3.7, v3.8, 3.9, 3.10 and 3.11
 104     private static final String AUTODETECT_DIRS =
 105             ";C:\\Program Files (x86)\\WiX Toolset v3.11\\bin;"
 106             + "C:\\Program Files\\WiX Toolset v3.11\\bin;"
 107             + "C:\\Program Files (x86)\\WiX Toolset v3.10\\bin;"
 108             + "C:\\Program Files\\WiX Toolset v3.10\\bin;"
 109             + "C:\\Program Files (x86)\\WiX Toolset v3.9\\bin;"
 110             + "C:\\Program Files\\WiX Toolset v3.9\\bin;"
 111             + "C:\\Program Files (x86)\\WiX Toolset v3.8\\bin;"
 112             + "C:\\Program Files\\WiX Toolset v3.8\\bin;"
 113             + "C:\\Program Files (x86)\\WiX Toolset v3.7\\bin;"
 114             + "C:\\Program Files\\WiX Toolset v3.7\\bin";
 115 
 116     public static final BundlerParamInfo<String> TOOL_CANDLE_EXECUTABLE =
 117             new WindowsBundlerParam<>(
 118             "win.msi.candle.exe",
 119             String.class,
 120             params -> {
 121                 for (String dirString : (System.getenv("PATH") +
 122                         AUTODETECT_DIRS).split(";")) {
 123                     File f = new File(dirString.replace("\"", ""), TOOL_CANDLE);
 124                     if (f.isFile()) {
 125                         return f.toString();
 126                     }
 127                 }
 128                 return null;
 129             },
 130             null);
 131 
 132     public static final BundlerParamInfo<String> TOOL_LIGHT_EXECUTABLE =
 133             new WindowsBundlerParam<>(
 134             "win.msi.light.exe",
 135             String.class,
 136             params -> {
 137                 for (String dirString : (System.getenv("PATH") +
 138                         AUTODETECT_DIRS).split(";")) {
 139                     File f = new File(dirString.replace("\"", ""), TOOL_LIGHT);
 140                     if (f.isFile()) {
 141                         return f.toString();
 142                     }
 143                 }
 144                 return null;
 145             },
 146             null);
 147 
 148     public static final StandardBundlerParam<Boolean> MENU_HINT =
 149         new WindowsBundlerParam<>(
 150                 Arguments.CLIOptions.WIN_MENU_HINT.getId(),
 151                 Boolean.class,
 152                 params -> false,
 153                 // valueOf(null) is false,
 154                 // and we actually do want null in some cases
 155                 (s, p) -> (s == null ||
 156                         "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
 157         );
 158 
 159     public static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
 160         new WindowsBundlerParam<>(
 161                 Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(),
 162                 Boolean.class,
 163                 params -> false,
 164                 // valueOf(null) is false,
 165                 // and we actually do want null in some cases
 166                 (s, p) -> (s == null ||
 167                        "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
 168         );
 169 
 170     @Override
 171     public String getName() {
 172         return I18N.getString("msi.bundler.name");
 173     }
 174 
 175     @Override
 176     public String getDescription() {
 177         return I18N.getString("msi.bundler.description");
 178     }
 179 
 180     @Override
 181     public String getID() {
 182         return "msi";
 183     }
 184 
 185     @Override
 186     public String getBundleType() {
 187         return "INSTALLER";
 188     }
 189 
 190     @Override
 191     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 192         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 193         results.addAll(WinAppBundler.getAppBundleParameters());
 194         results.addAll(getMsiBundleParameters());
 195         return results;
 196     }
 197 
 198     public static Collection<BundlerParamInfo<?>> getMsiBundleParameters() {
 199         return Arrays.asList(
 200                 DESCRIPTION,
 201                 MENU_GROUP,
 202                 MENU_HINT,
 203                 PRODUCT_VERSION,
 204                 SHORTCUT_HINT,
 205                 MSI_SYSTEM_WIDE,
 206                 VENDOR,
 207                 LICENSE_FILE,
 208                 INSTALLDIR_CHOOSER
 209         );
 210     }
 211 
 212     @Override
 213     public File execute(Map<String, ? super Object> params,
 214             File outputParentDir) throws PackagerException {
 215         return bundle(params, outputParentDir);
 216     }
 217 
 218     @Override
 219     public boolean supported(boolean platformInstaller) {
 220         return (Platform.getPlatform() == Platform.WINDOWS);
 221     }
 222 
 223     private static String findToolVersion(String toolName) {
 224         try {
 225             if (toolName == null || "".equals(toolName)) return null;
 226 
 227             ProcessBuilder pb = new ProcessBuilder(
 228                     toolName,
 229                     "/?");
 230             VersionExtractor ve = new VersionExtractor("version (\\d+.\\d+)");
 231             // not interested in the output
 232             IOUtils.exec(pb, true, ve);
 233             String version = ve.getVersion();
 234             Log.verbose(MessageFormat.format(
 235                     I18N.getString("message.tool-version"),
 236                     toolName, version));
 237             return version;
 238         } catch (Exception e) {
 239             if (Log.isDebug()) {
 240                 Log.verbose(e);
 241             }
 242             return null;
 243         }
 244     }
 245 
 246     @Override
 247     public boolean validate(Map<String, ? super Object> p)
 248             throws UnsupportedPlatformException, ConfigException {
 249         try {
 250             if (p == null) throw new ConfigException(
 251                     I18N.getString("error.parameters-null"),
 252                     I18N.getString("error.parameters-null.advice"));
 253 
 254             // run basic validation to ensure requirements are met
 255             // we are not interested in return code, only possible exception
 256             APP_BUNDLER.fetchFrom(p).validate(p);
 257 
 258             String candleVersion =
 259                     findToolVersion(TOOL_CANDLE_EXECUTABLE.fetchFrom(p));
 260             String lightVersion =
 261                     findToolVersion(TOOL_LIGHT_EXECUTABLE.fetchFrom(p));
 262 
 263             // WiX 3.0+ is required
 264             String minVersion = "3.0";
 265             boolean bad = false;
 266 
 267             if (VersionExtractor.isLessThan(candleVersion, minVersion)) {
 268                 Log.verbose(MessageFormat.format(
 269                         I18N.getString("message.wrong-tool-version"),
 270                         TOOL_CANDLE, candleVersion, minVersion));
 271                 bad = true;
 272             }
 273             if (VersionExtractor.isLessThan(lightVersion, minVersion)) {
 274                 Log.verbose(MessageFormat.format(
 275                         I18N.getString("message.wrong-tool-version"),
 276                         TOOL_LIGHT, lightVersion, minVersion));
 277                 bad = true;
 278             }
 279 
 280             if (bad){
 281                 throw new ConfigException(
 282                         I18N.getString("error.no-wix-tools"),
 283                         I18N.getString("error.no-wix-tools.advice"));
 284             }
 285 
 286             if (!VersionExtractor.isLessThan(lightVersion, "3.6")) {
 287                 Log.verbose(I18N.getString("message.use-wix36-features"));
 288                 p.put(CAN_USE_WIX36.getID(), Boolean.TRUE);
 289             }
 290 
 291             /********* validate bundle parameters *************/
 292 
 293             String version = PRODUCT_VERSION.fetchFrom(p);
 294             if (!isVersionStringValid(version)) {
 295                 throw new ConfigException(
 296                         MessageFormat.format(I18N.getString(
 297                                 "error.version-string-wrong-format"), version),
 298                         MessageFormat.format(I18N.getString(
 299                                 "error.version-string-wrong-format.advice"),
 300                                 PRODUCT_VERSION.getID()));
 301             }
 302 
 303             // only one mime type per association, at least one file extension
 304             List<Map<String, ? super Object>> associations =
 305                     FILE_ASSOCIATIONS.fetchFrom(p);
 306             if (associations != null) {
 307                 for (int i = 0; i < associations.size(); i++) {
 308                     Map<String, ? super Object> assoc = associations.get(i);
 309                     List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
 310                     if (mimes.size() > 1) {
 311                         throw new ConfigException(MessageFormat.format(
 312                                 I18N.getString("error.too-many-content-"
 313                                 + "types-for-file-association"), i),
 314                                 I18N.getString("error.too-many-content-"
 315                                 + "types-for-file-association.advice"));
 316                     }
 317                 }
 318             }
 319 
 320             return true;
 321         } catch (RuntimeException re) {
 322             if (re.getCause() instanceof ConfigException) {
 323                 throw (ConfigException) re.getCause();
 324             } else {
 325                 throw new ConfigException(re);
 326             }
 327         }
 328     }
 329 
 330     // http://msdn.microsoft.com/en-us/library/aa370859%28v=VS.85%29.aspx
 331     // The format of the string is as follows:
 332     //     major.minor.build
 333     // The first field is the major version and has a maximum value of 255.
 334     // The second field is the minor version and has a maximum value of 255.
 335     // The third field is called the build version or the update version and
 336     // has a maximum value of 65,535.
 337     static boolean isVersionStringValid(String v) {
 338         if (v == null) {
 339             return true;
 340         }
 341 
 342         String p[] = v.split("\\.");
 343         if (p.length > 3) {
 344             Log.verbose(I18N.getString(
 345                     "message.version-string-too-many-components"));
 346             return false;
 347         }
 348 
 349         try {
 350             int val = Integer.parseInt(p[0]);
 351             if (val < 0 || val > 255) {
 352                 Log.verbose(I18N.getString(
 353                         "error.version-string-major-out-of-range"));
 354                 return false;
 355             }
 356             if (p.length > 1) {
 357                 val = Integer.parseInt(p[1]);
 358                 if (val < 0 || val > 255) {
 359                     Log.verbose(I18N.getString(
 360                             "error.version-string-minor-out-of-range"));
 361                     return false;
 362                 }
 363             }
 364             if (p.length > 2) {
 365                 val = Integer.parseInt(p[2]);
 366                 if (val < 0 || val > 65535) {
 367                     Log.verbose(I18N.getString(
 368                             "error.version-string-build-out-of-range"));
 369                     return false;
 370                 }
 371             }
 372         } catch (NumberFormatException ne) {
 373             Log.verbose(I18N.getString("error.version-string-part-not-number"));
 374             Log.verbose(ne);
 375             return false;
 376         }
 377 
 378         return true;
 379     }
 380 
 381     private boolean prepareProto(Map<String, ? super Object> p)
 382                 throws PackagerException, IOException {
 383         File appImage = StandardBundlerParam.getPredefinedAppImage(p);
 384         File appDir = null;
 385 
 386         // we either have an application image or need to build one
 387         if (appImage != null) {
 388             appDir = new File(
 389                     MSI_IMAGE_DIR.fetchFrom(p), APP_NAME.fetchFrom(p));
 390             // copy everything from appImage dir into appDir/name
 391             IOUtils.copyRecursive(appImage.toPath(), appDir.toPath());
 392         } else {
 393             appDir = APP_BUNDLER.fetchFrom(p).doBundle(p,
 394                     MSI_IMAGE_DIR.fetchFrom(p), true);
 395         }
 396 
 397         p.put(WIN_APP_IMAGE.getID(), appDir);
 398 
 399         String licenseFile = LICENSE_FILE.fetchFrom(p);
 400         if (licenseFile != null) {
 401             // need to copy license file to the working directory and convert to rtf if needed
 402             File lfile = new File(licenseFile);
 403             File destFile = new File(CONFIG_ROOT.fetchFrom(p), lfile.getName());
 404             IOUtils.copyFile(lfile, destFile);
 405             ensureByMutationFileIsRTF(destFile);
 406         }
 407 
 408         // copy file association icons
 409         List<Map<String, ? super Object>> fileAssociations =
 410                 FILE_ASSOCIATIONS.fetchFrom(p);
 411         for (Map<String, ? super Object> fa : fileAssociations) {
 412             File icon = FA_ICON.fetchFrom(fa); // TODO FA_ICON_ICO
 413             if (icon == null) {
 414                 continue;
 415             }
 416 
 417             File faIconFile = new File(appDir, icon.getName());
 418 
 419             if (icon.exists()) {
 420                 try {
 421                     IOUtils.copyFile(icon, faIconFile);
 422                 } catch (IOException e) {
 423                     Log.verbose(e);
 424                 }
 425             }
 426         }
 427 
 428         return appDir != null;
 429     }
 430 
 431     public File bundle(Map<String, ? super Object> p, File outdir)
 432             throws PackagerException {
 433         if (!outdir.isDirectory() && !outdir.mkdirs()) {
 434             throw new PackagerException("error.cannot-create-output-dir",
 435                     outdir.getAbsolutePath());
 436         }
 437         if (!outdir.canWrite()) {
 438             throw new PackagerException("error.cannot-write-to-output-dir",
 439                     outdir.getAbsolutePath());
 440         }
 441 
 442         // validate we have valid tools before continuing
 443         String light = TOOL_LIGHT_EXECUTABLE.fetchFrom(p);
 444         String candle = TOOL_CANDLE_EXECUTABLE.fetchFrom(p);
 445         if (light == null || !new File(light).isFile() ||
 446             candle == null || !new File(candle).isFile()) {
 447             Log.verbose(MessageFormat.format(
 448                    I18N.getString("message.light-file-string"), light));
 449             Log.verbose(MessageFormat.format(
 450                    I18N.getString("message.candle-file-string"), candle));
 451             throw new PackagerException("error.no-wix-tools");
 452         }
 453 
 454         File imageDir = MSI_IMAGE_DIR.fetchFrom(p);
 455         try {
 456             imageDir.mkdirs();
 457 
 458             boolean menuShortcut = MENU_HINT.fetchFrom(p);
 459             boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p);
 460             if (!menuShortcut && !desktopShortcut) {
 461                 // both can not be false - user will not find the app
 462                 Log.verbose(I18N.getString("message.one-shortcut-required"));
 463                 p.put(MENU_HINT.getID(), true);
 464             }
 465 
 466             if (prepareProto(p) && prepareWiXConfig(p)
 467                     && prepareBasicProjectConfig(p)) {
 468                 File configScriptSrc = getConfig_Script(p);
 469                 if (configScriptSrc.exists()) {
 470                     // we need to be running post script in the image folder
 471 
 472                     // NOTE: Would it be better to generate it to the image
 473                     // folder and save only if "verbose" is requested?
 474 
 475                     // for now we replicate it
 476                     File configScript =
 477                         new File(imageDir, configScriptSrc.getName());
 478                     IOUtils.copyFile(configScriptSrc, configScript);
 479                     Log.verbose(MessageFormat.format(
 480                             I18N.getString("message.running-wsh-script"),
 481                             configScript.getAbsolutePath()));
 482                     IOUtils.run("wscript", configScript);
 483                 }
 484                 return buildMSI(p, outdir);
 485             }
 486             return null;
 487         } catch (IOException ex) {
 488             Log.verbose(ex);
 489             throw new PackagerException(ex);
 490         }
 491     }
 492 
 493     // name of post-image script
 494     private File getConfig_Script(Map<String, ? super Object> params) {
 495         return new File(CONFIG_ROOT.fetchFrom(params),
 496                 APP_NAME.fetchFrom(params) + "-post-image.wsf");
 497     }
 498 
 499     private boolean prepareBasicProjectConfig(
 500         Map<String, ? super Object> params) throws IOException {
 501         fetchResource(getConfig_Script(params).getName(),
 502                 I18N.getString("resource.post-install-script"),
 503                 (String) null,
 504                 getConfig_Script(params),
 505                 VERBOSE.fetchFrom(params),
 506                 RESOURCE_DIR.fetchFrom(params));
 507         return true;
 508     }
 509 
 510     private String relativePath(File basedir, File file) {
 511         return file.getAbsolutePath().substring(
 512                 basedir.getAbsolutePath().length() + 1);
 513     }
 514 
 515     boolean prepareMainProjectFile(
 516             Map<String, ? super Object> params) throws IOException {
 517         Map<String, String> data = new HashMap<>();
 518 
 519         UUID productGUID = UUID.randomUUID();
 520 
 521         Log.verbose(MessageFormat.format(
 522                 I18N.getString("message.generated-product-guid"),
 523                 productGUID.toString()));
 524 
 525         // we use random GUID for product itself but
 526         // user provided for upgrade guid
 527         // Upgrade guid is important to decide whether it is an upgrade of
 528         // installed app.  I.e. we need it to be the same for
 529         // 2 different versions of app if possible
 530         data.put("PRODUCT_GUID", productGUID.toString());
 531         data.put("PRODUCT_UPGRADE_GUID",
 532                 UPGRADE_UUID.fetchFrom(params).toString());
 533         data.put("UPGRADE_BLOCK", getUpgradeBlock(params));
 534 
 535         data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params));
 536         data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
 537         data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
 538         data.put("APPLICATION_VERSION", PRODUCT_VERSION.fetchFrom(params));
 539 
 540         // WinAppBundler will add application folder again => step out
 541         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
 542         File launcher = new File(imageRootDir,
 543                 WinAppBundler.getLauncherName(params));
 544 
 545         String launcherPath = relativePath(imageRootDir, launcher);
 546         data.put("APPLICATION_LAUNCHER", launcherPath);
 547 
 548         String iconPath = launcherPath.replace(".exe", ".ico");
 549 
 550         data.put("APPLICATION_ICON", iconPath);
 551 
 552         data.put("REGISTRY_ROOT", getRegistryRoot(params));
 553 
 554         boolean canUseWix36Features = CAN_USE_WIX36.fetchFrom(params);
 555         data.put("WIX36_ONLY_START",
 556                 canUseWix36Features ? "" : "<!--");
 557         data.put("WIX36_ONLY_END",
 558                 canUseWix36Features ? "" : "-->");
 559 
 560         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 561             data.put("INSTALL_SCOPE", "perMachine");
 562         } else {
 563             data.put("INSTALL_SCOPE", "perUser");
 564         }
 565 
 566         data.put("PLATFORM", "x64");
 567         data.put("WIN64", "yes");
 568 
 569         data.put("UI_BLOCK", getUIBlock(params));
 570 
 571         // Add CA to check install dir
 572         if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
 573             data.put("CA_BLOCK", CA_BLOCK);
 574             data.put("INVALID_INSTALL_DIR_DLG_BLOCK", INVALID_INSTALL_DIR_DLG_BLOCK);
 575         } else {
 576             data.put("CA_BLOCK", "");
 577             data.put("INVALID_INSTALL_DIR_DLG_BLOCK", "");
 578         }
 579 
 580         List<Map<String, ? super Object>> addLaunchers =
 581                 ADD_LAUNCHERS.fetchFrom(params);
 582 
 583         StringBuilder addLauncherIcons = new StringBuilder();
 584         for (int i = 0; i < addLaunchers.size(); i++) {
 585             Map<String, ? super Object> sl = addLaunchers.get(i);
 586             // <Icon Id="DesktopIcon.exe" SourceFile="APPLICATION_ICON" />
 587             if (SHORTCUT_HINT.fetchFrom(sl) || MENU_HINT.fetchFrom(sl)) {
 588                 File addLauncher = new File(imageRootDir,
 589                         WinAppBundler.getLauncherName(sl));
 590                 String addLauncherPath =
 591                         relativePath(imageRootDir, addLauncher);
 592                 String addLauncherIconPath =
 593                         addLauncherPath.replace(".exe", ".ico");
 594 
 595                 addLauncherIcons.append("        <Icon Id=\"Launcher");
 596                 addLauncherIcons.append(i);
 597                 addLauncherIcons.append(".exe\" SourceFile=\"");
 598                 addLauncherIcons.append(addLauncherIconPath);
 599                 addLauncherIcons.append("\" />\r\n");
 600             }
 601         }
 602         data.put("ADD_LAUNCHER_ICONS", addLauncherIcons.toString());
 603 
 604         String wxs = StandardBundlerParam.isRuntimeInstaller(params) ?
 605                 MSI_PROJECT_TEMPLATE_SERVER_JRE : MSI_PROJECT_TEMPLATE;
 606 
 607         try (Writer w = new BufferedWriter(
 608                 new FileWriter(getConfig_ProjectFile(params)))) {
 609 
 610             String content = preprocessTextResource(
 611                     getConfig_ProjectFile(params).getName(),
 612                     I18N.getString("resource.wix-config-file"),
 613                     wxs, data, VERBOSE.fetchFrom(params),
 614                     RESOURCE_DIR.fetchFrom(params));
 615             w.write(content);
 616         }
 617         return true;
 618     }
 619     private int id;
 620     private int compId;
 621     private final static String LAUNCHER_ID = "LauncherId";
 622 
 623     private static final String CA_BLOCK =
 624             "<Binary Id=\"CustomActionDLL\" SourceFile=\"wixhelper.dll\" />\n" +
 625             "<CustomAction Id=\"CHECK_INSTALLDIR\" BinaryKey=\"CustomActionDLL\" " +
 626             "DllEntry=\"CheckInstallDir\" />";
 627 
 628     private static final String INVALID_INSTALL_DIR_DLG_BLOCK =
 629             "<Dialog Id=\"InvalidInstallDir\" Width=\"300\" Height=\"85\" " +
 630             "Title=\"[ProductName] Setup\" NoMinimize=\"yes\">\n" +
 631             "<Control Id=\"InvalidInstallDirYes\" Type=\"PushButton\" X=\"100\" Y=\"55\" " +
 632             "Width=\"50\" Height=\"15\" Default=\"no\" Cancel=\"no\" Text=\"Yes\">\n" +
 633             "<Publish Event=\"NewDialog\" Value=\"VerifyReadyDlg\">1</Publish>\n" +
 634             "</Control>\n" +
 635             "<Control Id=\"InvalidInstallDirNo\" Type=\"PushButton\" X=\"150\" Y=\"55\" " +
 636             "Width=\"50\" Height=\"15\" Default=\"yes\" Cancel=\"yes\" Text=\"No\">\n" +
 637             "<Publish Event=\"NewDialog\" Value=\"InstallDirDlg\">1</Publish>\n" +
 638             "</Control>\n" +
 639             "<Control Id=\"Text\" Type=\"Text\" X=\"25\" Y=\"15\" Width=\"250\" Height=\"30\" " +
 640             "TabSkip=\"no\">\n" +
 641             "<Text>" + I18N.getString("message.install.dir.exist") + "</Text>\n" +
 642             "</Control>\n" +
 643             "</Dialog>";
 644 
 645     /**
 646      * Overrides the dialog sequence in built-in dialog set "WixUI_InstallDir"
 647      * to exclude license dialog
 648      */
 649     private static final String TWEAK_FOR_EXCLUDING_LICENSE =
 650               "     <Publish Dialog=\"WelcomeDlg\" Control=\"Next\""
 651             + "              Event=\"NewDialog\" Value=\"InstallDirDlg\""
 652             + " Order=\"2\">1</Publish>\n"
 653             + "     <Publish Dialog=\"InstallDirDlg\" Control=\"Back\""
 654             + "              Event=\"NewDialog\" Value=\"WelcomeDlg\""
 655             + " Order=\"2\">1</Publish>\n";
 656 
 657     private static final String CHECK_INSTALL_DLG_CTRL =
 658               "     <Publish Dialog=\"InstallDirDlg\" Control=\"Next\""
 659             + "              Event=\"DoAction\" Value=\"CHECK_INSTALLDIR\""
 660             + " Order=\"3\">1</Publish>\n"
 661             + "     <Publish Dialog=\"InstallDirDlg\" Control=\"Next\""
 662             + "              Event=\"NewDialog\" Value=\"InvalidInstallDir\""
 663             + " Order=\"5\">INSTALLDIR_VALID=\"0\"</Publish>\n"
 664             + "     <Publish Dialog=\"InstallDirDlg\" Control=\"Next\""
 665             + "              Event=\"NewDialog\" Value=\"VerifyReadyDlg\""
 666             + " Order=\"5\">INSTALLDIR_VALID=\"1\"</Publish>\n";
 667 
 668     // Required upgrade element for installers which support major upgrade (when user
 669     // specifies --win-upgrade-uuid). We will allow downgrades.
 670     private static final String UPGRADE_BLOCK =
 671             "<MajorUpgrade AllowDowngrades=\"yes\"/>";
 672 
 673     private String getUpgradeBlock(Map<String, ? super Object> params) {
 674         if (UPGRADE_UUID.getIsDefaultValue()) {
 675             return "";
 676         } else {
 677             return UPGRADE_BLOCK;
 678         }
 679     }
 680 
 681     /**
 682      * Creates UI element using WiX built-in dialog sets
 683      *     - WixUI_InstallDir/WixUI_Minimal.
 684      * The dialog sets are the closest to what we want to implement.
 685      *
 686      * WixUI_Minimal for license dialog only
 687      * WixUI_InstallDir for installdir dialog only or for both
 688      * installdir/license dialogs
 689      */
 690     private String getUIBlock(Map<String, ? super Object> params) throws IOException {
 691         String uiBlock = ""; // UI-less element
 692 
 693         // Copy CA dll to include with installer
 694         if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
 695             File helper = new File(CONFIG_ROOT.fetchFrom(params), "wixhelper.dll");
 696             try (InputStream is_lib = getResourceAsStream("wixhelper.dll")) {
 697                 Files.copy(is_lib, helper.toPath());
 698             }
 699         }
 700 
 701         if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
 702             boolean enableTweakForExcludingLicense =
 703                     (getLicenseFile(params) == null);
 704             uiBlock = "     <Property Id=\"WIXUI_INSTALLDIR\""
 705                     + " Value=\"APPLICATIONFOLDER\" />\n"
 706                     + "     <UIRef Id=\"WixUI_InstallDir\" />\n"
 707                     + (enableTweakForExcludingLicense ?
 708                             TWEAK_FOR_EXCLUDING_LICENSE : "")
 709                     + CHECK_INSTALL_DLG_CTRL;
 710         } else if (getLicenseFile(params) != null) {
 711             uiBlock = "     <UIRef Id=\"WixUI_Minimal\" />\n";
 712         }
 713 
 714         return uiBlock;
 715     }
 716 
 717     private void walkFileTree(Map<String, ? super Object> params,
 718             File root, PrintStream out, String prefix) {
 719         List<File> dirs = new ArrayList<>();
 720         List<File> files = new ArrayList<>();
 721 
 722         if (!root.isDirectory()) {
 723             throw new RuntimeException(
 724                     MessageFormat.format(
 725                             I18N.getString("error.cannot-walk-directory"),
 726                             root.getAbsolutePath()));
 727         }
 728 
 729         // sort to files and dirs
 730         File[] children = root.listFiles();
 731         if (children != null) {
 732             for (File f : children) {
 733                 if (f.isDirectory()) {
 734                     dirs.add(f);
 735                 } else {
 736                     files.add(f);
 737                 }
 738             }
 739         }
 740 
 741         // have files => need to output component
 742         out.println(prefix + " <Component Id=\"comp" + (compId++)
 743                 + "\" DiskId=\"1\""
 744                 + " Guid=\"" + UUID.randomUUID().toString() + "\""
 745                 + " Win64=\"yes\""
 746                 + ">");
 747         out.println(prefix + "  <CreateFolder/>");
 748         out.println(prefix + "  <RemoveFolder Id=\"RemoveDir"
 749                 + (id++) + "\" On=\"uninstall\" />");
 750 
 751         boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params);
 752         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
 753         File launcherFile =
 754                 new File(imageRootDir, WinAppBundler.getLauncherName(params));
 755 
 756         // Find out if we need to use registry. We need it if
 757         //  - we doing user level install as file can not serve as KeyPath
 758         //  - if we adding shortcut in this component
 759 
 760         for (File f: files) {
 761             boolean isLauncher = f.equals(launcherFile);
 762             if (isLauncher) {
 763                 needRegistryKey = true;
 764             }
 765         }
 766 
 767         if (needRegistryKey) {
 768             // has to be under HKCU to make WiX happy
 769             out.println(prefix + "    <RegistryKey Root=\"HKCU\" "
 770                     + " Key=\"Software\\" + VENDOR.fetchFrom(params) + "\\"
 771                     + APP_NAME.fetchFrom(params) + "\""
 772                     + (CAN_USE_WIX36.fetchFrom(params) ?
 773                     ">" : " Action=\"createAndRemoveOnUninstall\">"));
 774             out.println(prefix
 775                     + "     <RegistryValue Name=\"Version\" Value=\""
 776                     + VERSION.fetchFrom(params)
 777                     + "\" Type=\"string\" KeyPath=\"yes\"/>");
 778             out.println(prefix + "   </RegistryKey>");
 779         }
 780 
 781         boolean menuShortcut = MENU_HINT.fetchFrom(params);
 782         boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
 783 
 784         Map<String, String> idToFileMap = new TreeMap<>();
 785         boolean launcherSet = false;
 786 
 787         for (File f : files) {
 788             boolean isLauncher = f.equals(launcherFile);
 789 
 790             launcherSet = launcherSet || isLauncher;
 791 
 792             boolean doShortcuts =
 793                 isLauncher && (menuShortcut || desktopShortcut);
 794 
 795             String thisFileId = isLauncher ? LAUNCHER_ID : ("FileId" + (id++));
 796             idToFileMap.put(f.getName(), thisFileId);
 797 
 798             out.println(prefix + "   <File Id=\"" +
 799                     thisFileId + "\""
 800                     + " Name=\"" + f.getName() + "\" "
 801                     + " Source=\"" + relativePath(imageRootDir, f) + "\""
 802                     + " ProcessorArchitecture=\"x64\"" + ">");
 803             if (doShortcuts && desktopShortcut) {
 804                 out.println(prefix
 805                         + "  <Shortcut Id=\"desktopShortcut\" Directory="
 806                         + "\"DesktopFolder\""
 807                         + " Name=\"" + APP_NAME.fetchFrom(params)
 808                         + "\" WorkingDirectory=\"INSTALLDIR\""
 809                         + " Advertise=\"no\" Icon=\"DesktopIcon.exe\""
 810                         + " IconIndex=\"0\" />");
 811             }
 812             if (doShortcuts && menuShortcut) {
 813                 out.println(prefix
 814                         + "     <Shortcut Id=\"ExeShortcut\" Directory="
 815                         + "\"ProgramMenuDir\""
 816                         + " Name=\"" + APP_NAME.fetchFrom(params)
 817                         + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe\""
 818                         + " IconIndex=\"0\" />");
 819             }
 820 
 821             List<Map<String, ? super Object>> addLaunchers =
 822                     ADD_LAUNCHERS.fetchFrom(params);
 823             for (int i = 0; i < addLaunchers.size(); i++) {
 824                 Map<String, ? super Object> sl = addLaunchers.get(i);
 825                 File addLauncherFile = new File(imageRootDir,
 826                         WinAppBundler.getLauncherName(sl));
 827                 if (f.equals(addLauncherFile)) {
 828                     if (SHORTCUT_HINT.fetchFrom(sl)) {
 829                         out.println(prefix
 830                                 + "  <Shortcut Id=\"desktopShortcut"
 831                                 + i + "\" Directory=\"DesktopFolder\""
 832                                 + " Name=\"" + APP_NAME.fetchFrom(sl)
 833                                 + "\" WorkingDirectory=\"INSTALLDIR\""
 834                                 + " Advertise=\"no\" Icon=\"Launcher"
 835                                 + i + ".exe\" IconIndex=\"0\" />");
 836                     }
 837                     if (MENU_HINT.fetchFrom(sl)) {
 838                         out.println(prefix
 839                                 + "     <Shortcut Id=\"ExeShortcut"
 840                                 + i + "\" Directory=\"ProgramMenuDir\""
 841                                 + " Name=\"" + APP_NAME.fetchFrom(sl)
 842                                 + "\" Advertise=\"no\" Icon=\"Launcher"
 843                                 + i + ".exe\" IconIndex=\"0\" />");
 844                         // Should we allow different menu groups?  Not for now.
 845                     }
 846                 }
 847             }
 848             out.println(prefix + "   </File>");
 849         }
 850 
 851         if (launcherSet) {
 852             List<Map<String, ? super Object>> fileAssociations =
 853                 FILE_ASSOCIATIONS.fetchFrom(params);
 854             String regName = APP_REGISTRY_NAME.fetchFrom(params);
 855             Set<String> defaultedMimes = new TreeSet<>();
 856             int count = 0;
 857             for (Map<String, ? super Object> fa : fileAssociations) {
 858                 String description = FA_DESCRIPTION.fetchFrom(fa);
 859                 List<String> extensions = FA_EXTENSIONS.fetchFrom(fa);
 860                 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fa);
 861                 File icon = FA_ICON.fetchFrom(fa); // TODO FA_ICON_ICO
 862 
 863                 String mime = (mimeTypes == null ||
 864                     mimeTypes.isEmpty()) ? null : mimeTypes.get(0);
 865 
 866                 if (extensions == null) {
 867                     Log.verbose(I18N.getString(
 868                           "message.creating-association-with-null-extension"));
 869 
 870                     String entryName = regName + "File";
 871                     if (count > 0) {
 872                         entryName += "." + count;
 873                     }
 874                     count++;
 875                     out.print(prefix + "   <ProgId Id='" + entryName
 876                             + "' Description='" + description + "'");
 877                     if (icon != null && icon.exists()) {
 878                         out.print(" Icon='" + idToFileMap.get(icon.getName())
 879                                 + "' IconIndex='0'");
 880                     }
 881                     out.println(" />");
 882                 } else {
 883                     for (String ext : extensions) {
 884                         String entryName = regName + "File";
 885                         if (count > 0) {
 886                             entryName += "." + count;
 887                         }
 888                         count++;
 889 
 890                         out.print(prefix + "   <ProgId Id='" + entryName
 891                                 + "' Description='" + description + "'");
 892                         if (icon != null && icon.exists()) {
 893                             out.print(" Icon='"
 894                                     + idToFileMap.get(icon.getName())
 895                                     + "' IconIndex='0'");
 896                         }
 897                         out.println(">");
 898 
 899                         if (extensions == null) {
 900                             Log.verbose(I18N.getString(
 901                             "message.creating-association-with-null-extension"));
 902                         } else {
 903                             out.print(prefix + "    <Extension Id='"
 904                                     + ext + "' Advertise='no'");
 905                             if (mime == null) {
 906                                 out.println(">");
 907                             } else {
 908                                 out.println(" ContentType='" + mime + "'>");
 909                                 if (!defaultedMimes.contains(mime)) {
 910                                     out.println(prefix
 911                                             + "      <MIME ContentType='"
 912                                             + mime + "' Default='yes' />");
 913                                     defaultedMimes.add(mime);
 914                                 }
 915                             }
 916                             out.println(prefix
 917                                     + "      <Verb Id='open' Command='Open' "
 918                                     + "TargetFile='" + LAUNCHER_ID
 919                                     + "' Argument='\"%1\"' />");
 920                             out.println(prefix + "    </Extension>");
 921                         }
 922                         out.println(prefix + "   </ProgId>");
 923                     }
 924                 }
 925             }
 926         }
 927 
 928         out.println(prefix + " </Component>");
 929 
 930         for (File d : dirs) {
 931             out.println(prefix + " <Directory Id=\"dirid" + (id++)
 932                     + "\" Name=\"" + d.getName() + "\">");
 933             walkFileTree(params, d, out, prefix + " ");
 934             out.println(prefix + " </Directory>");
 935         }
 936     }
 937 
 938     String getRegistryRoot(Map<String, ? super Object> params) {
 939         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 940             return "HKLM";
 941         } else {
 942             return "HKCU";
 943         }
 944     }
 945 
 946     boolean prepareContentList(Map<String, ? super Object> params)
 947             throws FileNotFoundException {
 948         File f = new File(
 949                 CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE);
 950 
 951         try (PrintStream out = new PrintStream(f)) {
 952 
 953             // opening
 954             out.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
 955             out.println("<Include>");
 956 
 957             out.println(" <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">");
 958             if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 959                 // install to programfiles
 960                 out.println("  <Directory Id=\"ProgramFiles64Folder\" "
 961                             + "Name=\"PFiles\">");
 962             } else {
 963                 // install to user folder
 964                 out.println(
 965                     "  <Directory Name=\"AppData\" Id=\"LocalAppDataFolder\">");
 966             }
 967 
 968             // We should get valid folder or subfolders
 969             String installDir = WINDOWS_INSTALL_DIR.fetchFrom(params);
 970             String [] installDirs = installDir.split(Pattern.quote("\\"));
 971             for (int i = 0; i < (installDirs.length - 1); i++)  {
 972                 out.println("   <Directory Id=\"SUBDIR" + i + "\" Name=\""
 973                     + installDirs[i] + "\">");
 974             }
 975 
 976             out.println("   <Directory Id=\"APPLICATIONFOLDER\" Name=\""
 977                     + installDirs[installDirs.length - 1] + "\">");
 978 
 979             // dynamic part
 980             id = 0;
 981             compId = 0; // reset counters
 982             walkFileTree(params, WIN_APP_IMAGE.fetchFrom(params), out, "    ");
 983 
 984             // closing
 985             for (int i = 0; i < installDirs.length; i++)  {
 986                 out.println("   </Directory>");
 987             }
 988             out.println("  </Directory>");
 989 
 990             // for shortcuts
 991             if (SHORTCUT_HINT.fetchFrom(params)) {
 992                 out.println("  <Directory Id=\"DesktopFolder\" />");
 993             }
 994             if (MENU_HINT.fetchFrom(params)) {
 995                 out.println("  <Directory Id=\"ProgramMenuFolder\">");
 996                 out.println("    <Directory Id=\"ProgramMenuDir\" Name=\""
 997                         + MENU_GROUP.fetchFrom(params) + "\">");
 998                 out.println("      <Component Id=\"comp" + (compId++) + "\""
 999                         + " Guid=\"" + UUID.randomUUID().toString() + "\""
1000                         + " Win64=\"yes\""
1001                         + ">");
1002                 out.println("        <RemoveFolder Id=\"ProgramMenuDir\" "
1003                         + "On=\"uninstall\" />");
1004                 // This has to be under HKCU to make WiX happy.
1005                 // There are numberous discussions on this amoung WiX users
1006                 // (if user A installs and user B uninstalls key is left behind)
1007                 // there are suggested workarounds but none are appealing.
1008                 // Leave it for now
1009                 out.println(
1010                         "         <RegistryValue Root=\"HKCU\" Key=\"Software\\"
1011                         + VENDOR.fetchFrom(params) + "\\"
1012                         + APP_NAME.fetchFrom(params)
1013                         + "\" Type=\"string\" Value=\"\" />");
1014                 out.println("      </Component>");
1015                 out.println("    </Directory>");
1016                 out.println(" </Directory>");
1017             }
1018 
1019             out.println(" </Directory>");
1020 
1021             out.println(" <Feature Id=\"DefaultFeature\" "
1022                     + "Title=\"Main Feature\" Level=\"1\">");
1023             for (int j = 0; j < compId; j++) {
1024                 out.println("    <ComponentRef Id=\"comp" + j + "\" />");
1025             }
1026             // component is defined in the template.wsx
1027             out.println(
1028                     "    <ComponentRef Id=\"CleanupMainApplicationFolder\" />");
1029             out.println(" </Feature>");
1030             out.println("</Include>");
1031 
1032         }
1033         return true;
1034     }
1035 
1036     private File getConfig_ProjectFile(Map<String, ? super Object> params) {
1037         return new File(CONFIG_ROOT.fetchFrom(params),
1038                 APP_NAME.fetchFrom(params) + ".wxs");
1039     }
1040 
1041     private String getLicenseFile(Map<String, ? super Object> p) {
1042         String licenseFile = LICENSE_FILE.fetchFrom(p);
1043         if (licenseFile != null) {
1044             File lfile = new File(licenseFile);
1045             File destFile = new File(CONFIG_ROOT.fetchFrom(p), lfile.getName());
1046             String filePath = destFile.getAbsolutePath();
1047             if (filePath.contains(" ")) {
1048                 return "\"" + filePath + "\"";
1049             } else {
1050                 return filePath;
1051             }
1052         }
1053 
1054         return null;
1055     }
1056 
1057     private boolean prepareWiXConfig(
1058             Map<String, ? super Object> params) throws IOException {
1059         return prepareMainProjectFile(params) && prepareContentList(params);
1060 
1061     }
1062     private final static String MSI_PROJECT_TEMPLATE = "template.wxs";
1063     private final static String MSI_PROJECT_TEMPLATE_SERVER_JRE =
1064             "template.jre.wxs";
1065     private final static String MSI_PROJECT_CONTENT_FILE = "bundle.wxi";
1066 
1067     private File buildMSI(Map<String, ? super Object> params, File outdir)
1068             throws IOException {
1069         File tmpDir = new File(TEMP_ROOT.fetchFrom(params), "tmp");
1070         File candleOut = new File(
1071                 tmpDir, APP_NAME.fetchFrom(params) +".wixobj");
1072         File msiOut = new File(
1073                 outdir, INSTALLER_FILE_NAME.fetchFrom(params) + ".msi");
1074 
1075         Log.verbose(MessageFormat.format(I18N.getString(
1076                 "message.preparing-msi-config"), msiOut.getAbsolutePath()));
1077 
1078         msiOut.getParentFile().mkdirs();
1079 
1080         // run candle
1081         ProcessBuilder pb = new ProcessBuilder(
1082                 TOOL_CANDLE_EXECUTABLE.fetchFrom(params),
1083                 "-nologo",
1084                 getConfig_ProjectFile(params).getAbsolutePath(),
1085                 "-ext", "WixUtilExtension",
1086                 "-out", candleOut.getAbsolutePath());
1087         pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
1088         IOUtils.exec(pb);
1089 
1090         Log.verbose(MessageFormat.format(I18N.getString(
1091                 "message.generating-msi"), msiOut.getAbsolutePath()));
1092 
1093         boolean enableLicenseUI = (getLicenseFile(params) != null);
1094         boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params);
1095 
1096         List<String> commandLine = new ArrayList<>();
1097 
1098         commandLine.add(TOOL_LIGHT_EXECUTABLE.fetchFrom(params));
1099         if (enableLicenseUI) {
1100             commandLine.add("-dWixUILicenseRtf="+getLicenseFile(params));
1101         }
1102         commandLine.add("-nologo");
1103         commandLine.add("-spdb");
1104         commandLine.add("-sice:60");
1105                 // ignore warnings due to "missing launcguage info" (ICE60)
1106         commandLine.add(candleOut.getAbsolutePath());
1107         commandLine.add("-ext");
1108         commandLine.add("WixUtilExtension");
1109         if (enableLicenseUI || enableInstalldirUI) {
1110             commandLine.add("-ext");
1111             commandLine.add("WixUIExtension.dll");
1112         }
1113 
1114         // Only needed if we using CA dll, so Wix can find it
1115         if (enableInstalldirUI) {
1116             commandLine.add("-b");
1117             commandLine.add(CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
1118         }
1119 
1120         commandLine.add("-out");
1121         commandLine.add(msiOut.getAbsolutePath());
1122 
1123         // create .msi
1124         pb = new ProcessBuilder(commandLine);
1125 
1126         pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
1127         IOUtils.exec(pb);
1128 
1129         candleOut.delete();
1130         IOUtils.deleteRecursive(tmpDir);
1131 
1132         return msiOut;
1133     }
1134 
1135     public static void ensureByMutationFileIsRTF(File f) {
1136         if (f == null || !f.isFile()) return;
1137 
1138         try {
1139             boolean existingLicenseIsRTF = false;
1140 
1141             try (FileInputStream fin = new FileInputStream(f)) {
1142                 byte[] firstBits = new byte[7];
1143 
1144                 if (fin.read(firstBits) == firstBits.length) {
1145                     String header = new String(firstBits);
1146                     existingLicenseIsRTF = "{\\rtf1\\".equals(header);
1147                 }
1148             }
1149 
1150             if (!existingLicenseIsRTF) {
1151                 List<String> oldLicense = Files.readAllLines(f.toPath());
1152                 try (Writer w = Files.newBufferedWriter(
1153                         f.toPath(), Charset.forName("Windows-1252"))) {
1154                     w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033"
1155                             + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n"
1156                             + "\\viewkind4\\uc1\\pard\\sa200\\sl276"
1157                             + "\\slmult1\\lang9\\fs20 ");
1158                     oldLicense.forEach(l -> {
1159                         try {
1160                             for (char c : l.toCharArray()) {
1161                                 // 0x00 <= ch < 0x20 Escaped (\'hh)
1162                                 // 0x20 <= ch < 0x80 Raw(non - escaped) char
1163                                 // 0x80 <= ch <= 0xFF Escaped(\ 'hh)
1164                                 // 0x5C, 0x7B, 0x7D (special RTF characters
1165                                 // \,{,})Escaped(\'hh)
1166                                 // ch > 0xff Escaped (\\ud###?)
1167                                 if (c < 0x10) {
1168                                     w.write("\\'0");
1169                                     w.write(Integer.toHexString(c));
1170                                 } else if (c > 0xff) {
1171                                     w.write("\\ud");
1172                                     w.write(Integer.toString(c));
1173                                     // \\uc1 is in the header and in effect
1174                                     // so we trail with a replacement char if
1175                                     // the font lacks that character - '?'
1176                                     w.write("?");
1177                                 } else if ((c < 0x20) || (c >= 0x80) ||
1178                                         (c == 0x5C) || (c == 0x7B) ||
1179                                         (c == 0x7D)) {
1180                                     w.write("\\'");
1181                                     w.write(Integer.toHexString(c));
1182                                 } else {
1183                                     w.write(c);
1184                                 }
1185                             }
1186                             // blank lines are interpreted as paragraph breaks
1187                             if (l.length() < 1) {
1188                                 w.write("\\par");
1189                             } else {
1190                                 w.write(" ");
1191                             }
1192                             w.write("\r\n");
1193                         } catch (IOException e) {
1194                             Log.verbose(e);
1195                         }
1196                     });
1197                     w.write("}\r\n");
1198                 }
1199             }
1200         } catch (IOException e) {
1201             Log.verbose(e);
1202         }
1203 
1204     }
1205 }