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