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, Log.isDebug(), 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",
 483                              configScript, false);
 484                 }
 485                 return buildMSI(p, outdir);
 486             }
 487             return null;
 488         } catch (IOException ex) {
 489             Log.verbose(ex);
 490             throw new PackagerException(ex);
 491         }
 492     }
 493 
 494     // name of post-image script
 495     private File getConfig_Script(Map<String, ? super Object> params) {
 496         return new File(CONFIG_ROOT.fetchFrom(params),
 497                 APP_NAME.fetchFrom(params) + "-post-image.wsf");
 498     }
 499 
 500     private boolean prepareBasicProjectConfig(
 501         Map<String, ? super Object> params) throws IOException {
 502         fetchResource(getConfig_Script(params).getName(),
 503                 I18N.getString("resource.post-install-script"),
 504                 (String) null,
 505                 getConfig_Script(params),
 506                 VERBOSE.fetchFrom(params),
 507                 RESOURCE_DIR.fetchFrom(params));
 508         return true;
 509     }
 510 
 511     private String relativePath(File basedir, File file) {
 512         return file.getAbsolutePath().substring(
 513                 basedir.getAbsolutePath().length() + 1);
 514     }
 515 
 516     boolean prepareMainProjectFile(
 517             Map<String, ? super Object> params) throws IOException {
 518         Map<String, String> data = new HashMap<>();
 519 
 520         UUID productGUID = UUID.randomUUID();
 521 
 522         Log.verbose(MessageFormat.format(
 523                 I18N.getString("message.generated-product-guid"),
 524                 productGUID.toString()));
 525 
 526         // we use random GUID for product itself but
 527         // user provided for upgrade guid
 528         // Upgrade guid is important to decide whether it is an upgrade of
 529         // installed app.  I.e. we need it to be the same for
 530         // 2 different versions of app if possible
 531         data.put("PRODUCT_GUID", productGUID.toString());
 532         data.put("PRODUCT_UPGRADE_GUID",
 533                 UPGRADE_UUID.fetchFrom(params).toString());
 534         data.put("UPGRADE_BLOCK", getUpgradeBlock(params));
 535 
 536         data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params));
 537         data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
 538         data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
 539         data.put("APPLICATION_VERSION", PRODUCT_VERSION.fetchFrom(params));
 540 
 541         // WinAppBundler will add application folder again => step out
 542         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
 543         File launcher = new File(imageRootDir,
 544                 WinAppBundler.getLauncherName(params));
 545 
 546         String launcherPath = relativePath(imageRootDir, launcher);
 547         data.put("APPLICATION_LAUNCHER", launcherPath);
 548 
 549         String iconPath = launcherPath.replace(".exe", ".ico");
 550 
 551         data.put("APPLICATION_ICON", iconPath);
 552 
 553         data.put("REGISTRY_ROOT", getRegistryRoot(params));
 554 
 555         boolean canUseWix36Features = CAN_USE_WIX36.fetchFrom(params);
 556         data.put("WIX36_ONLY_START",
 557                 canUseWix36Features ? "" : "<!--");
 558         data.put("WIX36_ONLY_END",
 559                 canUseWix36Features ? "" : "-->");
 560 
 561         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 562             data.put("INSTALL_SCOPE", "perMachine");
 563         } else {
 564             data.put("INSTALL_SCOPE", "perUser");
 565         }
 566 
 567         data.put("PLATFORM", "x64");
 568         data.put("WIN64", "yes");
 569 
 570         data.put("UI_BLOCK", getUIBlock(params));
 571 
 572         // Add CA to check install dir
 573         if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
 574             data.put("CA_BLOCK", CA_BLOCK);
 575             data.put("INVALID_INSTALL_DIR_DLG_BLOCK", INVALID_INSTALL_DIR_DLG_BLOCK);
 576         } else {
 577             data.put("CA_BLOCK", "");
 578             data.put("INVALID_INSTALL_DIR_DLG_BLOCK", "");
 579         }
 580 
 581         List<Map<String, ? super Object>> addLaunchers =
 582                 ADD_LAUNCHERS.fetchFrom(params);
 583 
 584         StringBuilder addLauncherIcons = new StringBuilder();
 585         for (int i = 0; i < addLaunchers.size(); i++) {
 586             Map<String, ? super Object> sl = addLaunchers.get(i);
 587             // <Icon Id="DesktopIcon.exe" SourceFile="APPLICATION_ICON" />
 588             if (SHORTCUT_HINT.fetchFrom(sl) || MENU_HINT.fetchFrom(sl)) {
 589                 File addLauncher = new File(imageRootDir,
 590                         WinAppBundler.getLauncherName(sl));
 591                 String addLauncherPath =
 592                         relativePath(imageRootDir, addLauncher);
 593                 String addLauncherIconPath =
 594                         addLauncherPath.replace(".exe", ".ico");
 595 
 596                 addLauncherIcons.append("        <Icon Id=\"Launcher");
 597                 addLauncherIcons.append(i);
 598                 addLauncherIcons.append(".exe\" SourceFile=\"");
 599                 addLauncherIcons.append(addLauncherIconPath);
 600                 addLauncherIcons.append("\" />\r\n");
 601             }
 602         }
 603         data.put("ADD_LAUNCHER_ICONS", addLauncherIcons.toString());
 604 
 605         String wxs = StandardBundlerParam.isRuntimeInstaller(params) ?
 606                 MSI_PROJECT_TEMPLATE_SERVER_JRE : MSI_PROJECT_TEMPLATE;
 607 
 608         Writer w = new BufferedWriter(
 609                 new FileWriter(getConfig_ProjectFile(params)));
 610 
 611         String content = preprocessTextResource(
 612                 getConfig_ProjectFile(params).getName(),
 613                 I18N.getString("resource.wix-config-file"),
 614                 wxs, data, VERBOSE.fetchFrom(params),
 615                 RESOURCE_DIR.fetchFrom(params));
 616         w.write(content);
 617         w.close();
 618         return true;
 619     }
 620     private int id;
 621     private int compId;
 622     private final static String LAUNCHER_ID = "LauncherId";
 623 
 624     private static final String CA_BLOCK =
 625             "<Binary Id=\"CustomActionDLL\" SourceFile=\"wixhelper.dll\" />\n" +
 626             "<CustomAction Id=\"CHECK_INSTALLDIR\" BinaryKey=\"CustomActionDLL\" " +
 627             "DllEntry=\"CheckInstallDir\" />";
 628 
 629     private static final String INVALID_INSTALL_DIR_DLG_BLOCK =
 630             "<Dialog Id=\"InvalidInstallDir\" Width=\"300\" Height=\"85\" " +
 631             "Title=\"[ProductName] Setup\" NoMinimize=\"yes\">\n" +
 632             "<Control Id=\"InvalidInstallDirYes\" Type=\"PushButton\" X=\"100\" Y=\"55\" " +
 633             "Width=\"50\" Height=\"15\" Default=\"no\" Cancel=\"no\" Text=\"Yes\">\n" +
 634             "<Publish Event=\"NewDialog\" Value=\"VerifyReadyDlg\">1</Publish>\n" +
 635             "</Control>\n" +
 636             "<Control Id=\"InvalidInstallDirNo\" Type=\"PushButton\" X=\"150\" Y=\"55\" " +
 637             "Width=\"50\" Height=\"15\" Default=\"yes\" Cancel=\"yes\" Text=\"No\">\n" +
 638             "<Publish Event=\"NewDialog\" Value=\"InstallDirDlg\">1</Publish>\n" +
 639             "</Control>\n" +
 640             "<Control Id=\"Text\" Type=\"Text\" X=\"25\" Y=\"15\" Width=\"250\" Height=\"30\" " +
 641             "TabSkip=\"no\">\n" +
 642             "<Text>" + I18N.getString("message.install.dir.exist") + "</Text>\n" +
 643             "</Control>\n" +
 644             "</Dialog>";
 645 
 646     /**
 647      * Overrides the dialog sequence in built-in dialog set "WixUI_InstallDir"
 648      * to exclude license dialog
 649      */
 650     private static final String TWEAK_FOR_EXCLUDING_LICENSE =
 651               "     <Publish Dialog=\"WelcomeDlg\" Control=\"Next\""
 652             + "              Event=\"NewDialog\" Value=\"InstallDirDlg\""
 653             + " Order=\"2\">1</Publish>\n"
 654             + "     <Publish Dialog=\"InstallDirDlg\" Control=\"Back\""
 655             + "              Event=\"NewDialog\" Value=\"WelcomeDlg\""
 656             + " Order=\"2\">1</Publish>\n";
 657 
 658     private static final String CHECK_INSTALL_DLG_CTRL =
 659               "     <Publish Dialog=\"InstallDirDlg\" Control=\"Next\""
 660             + "              Event=\"DoAction\" Value=\"CHECK_INSTALLDIR\""
 661             + " Order=\"3\">1</Publish>\n"
 662             + "     <Publish Dialog=\"InstallDirDlg\" Control=\"Next\""
 663             + "              Event=\"NewDialog\" Value=\"InvalidInstallDir\""
 664             + " Order=\"5\">INSTALLDIR_VALID=\"0\"</Publish>\n"
 665             + "     <Publish Dialog=\"InstallDirDlg\" Control=\"Next\""
 666             + "              Event=\"NewDialog\" Value=\"VerifyReadyDlg\""
 667             + " Order=\"5\">INSTALLDIR_VALID=\"1\"</Publish>\n";
 668 
 669     // Required upgrade element for installers which support major upgrade (when user
 670     // specifies --win-upgrade-uuid). We will allow downgrades.
 671     private static final String UPGRADE_BLOCK =
 672             "<MajorUpgrade AllowDowngrades=\"yes\"/>";
 673 
 674     private String getUpgradeBlock(Map<String, ? super Object> params) {
 675         if (UPGRADE_UUID.getIsDefaultValue()) {
 676             return "";
 677         } else {
 678             return UPGRADE_BLOCK;
 679         }
 680     }
 681 
 682     /**
 683      * Creates UI element using WiX built-in dialog sets
 684      *     - WixUI_InstallDir/WixUI_Minimal.
 685      * The dialog sets are the closest to what we want to implement.
 686      *
 687      * WixUI_Minimal for license dialog only
 688      * WixUI_InstallDir for installdir dialog only or for both
 689      * installdir/license dialogs
 690      */
 691     private String getUIBlock(Map<String, ? super Object> params) throws IOException {
 692         String uiBlock = ""; // UI-less element
 693 
 694         // Copy CA dll to include with installer
 695         if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
 696             File helper = new File(CONFIG_ROOT.fetchFrom(params), "wixhelper.dll");
 697             try (InputStream is_lib = getResourceAsStream("wixhelper.dll")) {
 698                 Files.copy(is_lib, helper.toPath());
 699             }
 700         }
 701 
 702         if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
 703             boolean enableTweakForExcludingLicense =
 704                     (getLicenseFile(params) == null);
 705             uiBlock = "     <Property Id=\"WIXUI_INSTALLDIR\""
 706                     + " Value=\"APPLICATIONFOLDER\" />\n"
 707                     + "     <UIRef Id=\"WixUI_InstallDir\" />\n"
 708                     + (enableTweakForExcludingLicense ?
 709                             TWEAK_FOR_EXCLUDING_LICENSE : "")
 710                     + CHECK_INSTALL_DLG_CTRL;
 711         } else if (getLicenseFile(params) != null) {
 712             uiBlock = "     <UIRef Id=\"WixUI_Minimal\" />\n";
 713         }
 714 
 715         return uiBlock;
 716     }
 717 
 718     private void walkFileTree(Map<String, ? super Object> params,
 719             File root, PrintStream out, String prefix) {
 720         List<File> dirs = new ArrayList<>();
 721         List<File> files = new ArrayList<>();
 722 
 723         if (!root.isDirectory()) {
 724             throw new RuntimeException(
 725                     MessageFormat.format(
 726                             I18N.getString("error.cannot-walk-directory"),
 727                             root.getAbsolutePath()));
 728         }
 729 
 730         // sort to files and dirs
 731         File[] children = root.listFiles();
 732         if (children != null) {
 733             for (File f : children) {
 734                 if (f.isDirectory()) {
 735                     dirs.add(f);
 736                 } else {
 737                     files.add(f);
 738                 }
 739             }
 740         }
 741 
 742         // have files => need to output component
 743         out.println(prefix + " <Component Id=\"comp" + (compId++)
 744                 + "\" DiskId=\"1\""
 745                 + " Guid=\"" + UUID.randomUUID().toString() + "\""
 746                 + " Win64=\"yes\""
 747                 + ">");
 748         out.println(prefix + "  <CreateFolder/>");
 749         out.println(prefix + "  <RemoveFolder Id=\"RemoveDir"
 750                 + (id++) + "\" On=\"uninstall\" />");
 751 
 752         boolean needRegistryKey = !MSI_SYSTEM_WIDE.fetchFrom(params);
 753         File imageRootDir = WIN_APP_IMAGE.fetchFrom(params);
 754         File launcherFile =
 755                 new File(imageRootDir, WinAppBundler.getLauncherName(params));
 756 
 757         // Find out if we need to use registry. We need it if
 758         //  - we doing user level install as file can not serve as KeyPath
 759         //  - if we adding shortcut in this component
 760 
 761         for (File f: files) {
 762             boolean isLauncher = f.equals(launcherFile);
 763             if (isLauncher) {
 764                 needRegistryKey = true;
 765             }
 766         }
 767 
 768         if (needRegistryKey) {
 769             // has to be under HKCU to make WiX happy
 770             out.println(prefix + "    <RegistryKey Root=\"HKCU\" "
 771                     + " Key=\"Software\\" + VENDOR.fetchFrom(params) + "\\"
 772                     + APP_NAME.fetchFrom(params) + "\""
 773                     + (CAN_USE_WIX36.fetchFrom(params) ?
 774                     ">" : " Action=\"createAndRemoveOnUninstall\">"));
 775             out.println(prefix
 776                     + "     <RegistryValue Name=\"Version\" Value=\""
 777                     + VERSION.fetchFrom(params)
 778                     + "\" Type=\"string\" KeyPath=\"yes\"/>");
 779             out.println(prefix + "   </RegistryKey>");
 780         }
 781 
 782         boolean menuShortcut = MENU_HINT.fetchFrom(params);
 783         boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(params);
 784 
 785         Map<String, String> idToFileMap = new TreeMap<>();
 786         boolean launcherSet = false;
 787 
 788         for (File f : files) {
 789             boolean isLauncher = f.equals(launcherFile);
 790 
 791             launcherSet = launcherSet || isLauncher;
 792 
 793             boolean doShortcuts =
 794                 isLauncher && (menuShortcut || desktopShortcut);
 795 
 796             String thisFileId = isLauncher ? LAUNCHER_ID : ("FileId" + (id++));
 797             idToFileMap.put(f.getName(), thisFileId);
 798 
 799             out.println(prefix + "   <File Id=\"" +
 800                     thisFileId + "\""
 801                     + " Name=\"" + f.getName() + "\" "
 802                     + " Source=\"" + relativePath(imageRootDir, f) + "\""
 803                     + " ProcessorArchitecture=\"x64\"" + ">");
 804             if (doShortcuts && desktopShortcut) {
 805                 out.println(prefix
 806                         + "  <Shortcut Id=\"desktopShortcut\" Directory="
 807                         + "\"DesktopFolder\""
 808                         + " Name=\"" + APP_NAME.fetchFrom(params)
 809                         + "\" WorkingDirectory=\"INSTALLDIR\""
 810                         + " Advertise=\"no\" Icon=\"DesktopIcon.exe\""
 811                         + " IconIndex=\"0\" />");
 812             }
 813             if (doShortcuts && menuShortcut) {
 814                 out.println(prefix
 815                         + "     <Shortcut Id=\"ExeShortcut\" Directory="
 816                         + "\"ProgramMenuDir\""
 817                         + " Name=\"" + APP_NAME.fetchFrom(params)
 818                         + "\" Advertise=\"no\" Icon=\"StartMenuIcon.exe\""
 819                         + " IconIndex=\"0\" />");
 820             }
 821 
 822             List<Map<String, ? super Object>> addLaunchers =
 823                     ADD_LAUNCHERS.fetchFrom(params);
 824             for (int i = 0; i < addLaunchers.size(); i++) {
 825                 Map<String, ? super Object> sl = addLaunchers.get(i);
 826                 File addLauncherFile = new File(imageRootDir,
 827                         WinAppBundler.getLauncherName(sl));
 828                 if (f.equals(addLauncherFile)) {
 829                     if (SHORTCUT_HINT.fetchFrom(sl)) {
 830                         out.println(prefix
 831                                 + "  <Shortcut Id=\"desktopShortcut"
 832                                 + i + "\" Directory=\"DesktopFolder\""
 833                                 + " Name=\"" + APP_NAME.fetchFrom(sl)
 834                                 + "\" WorkingDirectory=\"INSTALLDIR\""
 835                                 + " Advertise=\"no\" Icon=\"Launcher"
 836                                 + i + ".exe\" IconIndex=\"0\" />");
 837                     }
 838                     if (MENU_HINT.fetchFrom(sl)) {
 839                         out.println(prefix
 840                                 + "     <Shortcut Id=\"ExeShortcut"
 841                                 + i + "\" Directory=\"ProgramMenuDir\""
 842                                 + " Name=\"" + APP_NAME.fetchFrom(sl)
 843                                 + "\" Advertise=\"no\" Icon=\"Launcher"
 844                                 + i + ".exe\" IconIndex=\"0\" />");
 845                         // Should we allow different menu groups?  Not for now.
 846                     }
 847                 }
 848             }
 849             out.println(prefix + "   </File>");
 850         }
 851 
 852         if (launcherSet) {
 853             List<Map<String, ? super Object>> fileAssociations =
 854                 FILE_ASSOCIATIONS.fetchFrom(params);
 855             String regName = APP_REGISTRY_NAME.fetchFrom(params);
 856             Set<String> defaultedMimes = new TreeSet<>();
 857             int count = 0;
 858             for (Map<String, ? super Object> fa : fileAssociations) {
 859                 String description = FA_DESCRIPTION.fetchFrom(fa);
 860                 List<String> extensions = FA_EXTENSIONS.fetchFrom(fa);
 861                 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fa);
 862                 File icon = FA_ICON.fetchFrom(fa); // TODO FA_ICON_ICO
 863 
 864                 String mime = (mimeTypes == null ||
 865                     mimeTypes.isEmpty()) ? null : mimeTypes.get(0);
 866 
 867                 if (extensions == null) {
 868                     Log.verbose(I18N.getString(
 869                           "message.creating-association-with-null-extension"));
 870 
 871                     String entryName = regName + "File";
 872                     if (count > 0) {
 873                         entryName += "." + count;
 874                     }
 875                     count++;
 876                     out.print(prefix + "   <ProgId Id='" + entryName
 877                             + "' Description='" + description + "'");
 878                     if (icon != null && icon.exists()) {
 879                         out.print(" Icon='" + idToFileMap.get(icon.getName())
 880                                 + "' IconIndex='0'");
 881                     }
 882                     out.println(" />");
 883                 } else {
 884                     for (String ext : extensions) {
 885                         String entryName = regName + "File";
 886                         if (count > 0) {
 887                             entryName += "." + count;
 888                         }
 889                         count++;
 890 
 891                         out.print(prefix + "   <ProgId Id='" + entryName
 892                                 + "' Description='" + description + "'");
 893                         if (icon != null && icon.exists()) {
 894                             out.print(" Icon='"
 895                                     + idToFileMap.get(icon.getName())
 896                                     + "' IconIndex='0'");
 897                         }
 898                         out.println(">");
 899 
 900                         if (extensions == null) {
 901                             Log.verbose(I18N.getString(
 902                             "message.creating-association-with-null-extension"));
 903                         } else {
 904                             out.print(prefix + "    <Extension Id='"
 905                                     + ext + "' Advertise='no'");
 906                             if (mime == null) {
 907                                 out.println(">");
 908                             } else {
 909                                 out.println(" ContentType='" + mime + "'>");
 910                                 if (!defaultedMimes.contains(mime)) {
 911                                     out.println(prefix
 912                                             + "      <MIME ContentType='"
 913                                             + mime + "' Default='yes' />");
 914                                     defaultedMimes.add(mime);
 915                                 }
 916                             }
 917                             out.println(prefix
 918                                     + "      <Verb Id='open' Command='Open' "
 919                                     + "TargetFile='" + LAUNCHER_ID
 920                                     + "' Argument='\"%1\"' />");
 921                             out.println(prefix + "    </Extension>");
 922                         }
 923                         out.println(prefix + "   </ProgId>");
 924                     }
 925                 }
 926             }
 927         }
 928 
 929         out.println(prefix + " </Component>");
 930 
 931         for (File d : dirs) {
 932             out.println(prefix + " <Directory Id=\"dirid" + (id++)
 933                     + "\" Name=\"" + d.getName() + "\">");
 934             walkFileTree(params, d, out, prefix + " ");
 935             out.println(prefix + " </Directory>");
 936         }
 937     }
 938 
 939     String getRegistryRoot(Map<String, ? super Object> params) {
 940         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
 941             return "HKLM";
 942         } else {
 943             return "HKCU";
 944         }
 945     }
 946 
 947     boolean prepareContentList(Map<String, ? super Object> params)
 948             throws FileNotFoundException {
 949         File f = new File(
 950                 CONFIG_ROOT.fetchFrom(params), MSI_PROJECT_CONTENT_FILE);
 951         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 of them 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("    <ComponentRef Id=\"CleanupMainApplicationFolder\" />");
1028         out.println(" </Feature>");
1029         out.println("</Include>");
1030 
1031         out.close();
1032         return true;
1033     }
1034 
1035     private File getConfig_ProjectFile(Map<String, ? super Object> params) {
1036         return new File(CONFIG_ROOT.fetchFrom(params),
1037                 APP_NAME.fetchFrom(params) + ".wxs");
1038     }
1039 
1040     private String getLicenseFile(Map<String, ? super Object> p) {
1041         String licenseFile = LICENSE_FILE.fetchFrom(p);
1042         if (licenseFile != null) {
1043             File lfile = new File(licenseFile);
1044             File destFile = new File(CONFIG_ROOT.fetchFrom(p), lfile.getName());
1045             String filePath = destFile.getAbsolutePath();
1046             if (filePath.contains(" ")) {
1047                 return "\"" + filePath + "\"";
1048             } else {
1049                 return filePath;
1050             }
1051         }
1052 
1053         return null;
1054     }
1055 
1056     private boolean prepareWiXConfig(
1057             Map<String, ? super Object> params) throws IOException {
1058         return prepareMainProjectFile(params) && prepareContentList(params);
1059 
1060     }
1061     private final static String MSI_PROJECT_TEMPLATE = "template.wxs";
1062     private final static String MSI_PROJECT_TEMPLATE_SERVER_JRE =
1063             "template.jre.wxs";
1064     private final static String MSI_PROJECT_CONTENT_FILE = "bundle.wxi";
1065 
1066     private File buildMSI(Map<String, ? super Object> params, File outdir)
1067             throws IOException {
1068         File tmpDir = new File(TEMP_ROOT.fetchFrom(params), "tmp");
1069         File candleOut = new File(
1070                 tmpDir, APP_NAME.fetchFrom(params) +".wixobj");
1071         File msiOut = new File(
1072                 outdir, INSTALLER_FILE_NAME.fetchFrom(params) + ".msi");
1073 
1074         Log.verbose(MessageFormat.format(I18N.getString(
1075                 "message.preparing-msi-config"), msiOut.getAbsolutePath()));
1076 
1077         msiOut.getParentFile().mkdirs();
1078 
1079         // run candle
1080         ProcessBuilder pb = new ProcessBuilder(
1081                 TOOL_CANDLE_EXECUTABLE.fetchFrom(params),
1082                 "-nologo",
1083                 getConfig_ProjectFile(params).getAbsolutePath(),
1084                 "-ext", "WixUtilExtension",
1085                 "-out", candleOut.getAbsolutePath());
1086         pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
1087         IOUtils.exec(pb, false);
1088 
1089         Log.verbose(MessageFormat.format(I18N.getString(
1090                 "message.generating-msi"), msiOut.getAbsolutePath()));
1091 
1092         boolean enableLicenseUI = (getLicenseFile(params) != null);
1093         boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params);
1094 
1095         List<String> commandLine = new ArrayList<>();
1096 
1097         commandLine.add(TOOL_LIGHT_EXECUTABLE.fetchFrom(params));
1098         if (enableLicenseUI) {
1099             commandLine.add("-dWixUILicenseRtf="+getLicenseFile(params));
1100         }
1101         commandLine.add("-nologo");
1102         commandLine.add("-spdb");
1103         commandLine.add("-sice:60");
1104                 // ignore warnings due to "missing launcguage info" (ICE60)
1105         commandLine.add(candleOut.getAbsolutePath());
1106         commandLine.add("-ext");
1107         commandLine.add("WixUtilExtension");
1108         if (enableLicenseUI || enableInstalldirUI) {
1109             commandLine.add("-ext");
1110             commandLine.add("WixUIExtension.dll");
1111         }
1112 
1113         // Only needed if we using CA dll, so Wix can find it
1114         if (enableInstalldirUI) {
1115             commandLine.add("-b");
1116             commandLine.add(CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
1117         }
1118 
1119         commandLine.add("-out");
1120         commandLine.add(msiOut.getAbsolutePath());
1121 
1122         // create .msi
1123         pb = new ProcessBuilder(commandLine);
1124 
1125         pb = pb.directory(WIN_APP_IMAGE.fetchFrom(params));
1126         IOUtils.exec(pb, false);
1127 
1128         candleOut.delete();
1129         IOUtils.deleteRecursive(tmpDir);
1130 
1131         return msiOut;
1132     }
1133 
1134     public static void ensureByMutationFileIsRTF(File f) {
1135         if (f == null || !f.isFile()) return;
1136 
1137         try {
1138             boolean existingLicenseIsRTF = false;
1139 
1140             try (FileInputStream fin = new FileInputStream(f)) {
1141                 byte[] firstBits = new byte[7];
1142 
1143                 if (fin.read(firstBits) == firstBits.length) {
1144                     String header = new String(firstBits);
1145                     existingLicenseIsRTF = "{\\rtf1\\".equals(header);
1146                 }
1147             }
1148 
1149             if (!existingLicenseIsRTF) {
1150                 List<String> oldLicense = Files.readAllLines(f.toPath());
1151                 try (Writer w = Files.newBufferedWriter(
1152                         f.toPath(), Charset.forName("Windows-1252"))) {
1153                     w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033"
1154                             + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n"
1155                             + "\\viewkind4\\uc1\\pard\\sa200\\sl276"
1156                             + "\\slmult1\\lang9\\fs20 ");
1157                     oldLicense.forEach(l -> {
1158                         try {
1159                             for (char c : l.toCharArray()) {
1160                                 // 0x00 <= ch < 0x20 Escaped (\'hh)
1161                                 // 0x20 <= ch < 0x80 Raw(non - escaped) char
1162                                 // 0x80 <= ch <= 0xFF Escaped(\ 'hh)
1163                                 // 0x5C, 0x7B, 0x7D (special RTF characters
1164                                 // \,{,})Escaped(\'hh)
1165                                 // ch > 0xff Escaped (\\ud###?)
1166                                 if (c < 0x10) {
1167                                     w.write("\\'0");
1168                                     w.write(Integer.toHexString(c));
1169                                 } else if (c > 0xff) {
1170                                     w.write("\\ud");
1171                                     w.write(Integer.toString(c));
1172                                     // \\uc1 is in the header and in effect
1173                                     // so we trail with a replacement char if
1174                                     // the font lacks that character - '?'
1175                                     w.write("?");
1176                                 } else if ((c < 0x20) || (c >= 0x80) ||
1177                                         (c == 0x5C) || (c == 0x7B) ||
1178                                         (c == 0x7D)) {
1179                                     w.write("\\'");
1180                                     w.write(Integer.toHexString(c));
1181                                 } else {
1182                                     w.write(c);
1183                                 }
1184                             }
1185                             // blank lines are interpreted as paragraph breaks
1186                             if (l.length() < 1) {
1187                                 w.write("\\par");
1188                             } else {
1189                                 w.write(" ");
1190                             }
1191                             w.write("\r\n");
1192                         } catch (IOException e) {
1193                             Log.verbose(e);
1194                         }
1195                     });
1196                     w.write("}\r\n");
1197                 }
1198             }
1199         } catch (IOException e) {
1200             Log.verbose(e);
1201         }
1202 
1203     }
1204 }