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