src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java
branchJDK-8200758-branch
changeset 58994 b09ba68c6a19
parent 58993 b5e1baa9d2c3
child 58995 de1413ae214c
equal deleted inserted replaced
58993:b5e1baa9d2c3 58994:b09ba68c6a19
     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.charset.StandardCharsets;
       
    31 import java.nio.file.Files;
       
    32 import java.nio.file.Path;
       
    33 import java.nio.file.Paths;
       
    34 import java.text.MessageFormat;
       
    35 import java.util.*;
       
    36 import java.util.regex.Pattern;
       
    37 import java.util.stream.Collectors;
       
    38 import java.util.stream.Stream;
       
    39 import javax.xml.stream.XMLOutputFactory;
       
    40 import javax.xml.stream.XMLStreamException;
       
    41 import javax.xml.stream.XMLStreamWriter;
       
    42 import static jdk.jpackage.internal.OverridableResource.createResource;
       
    43 import static jdk.jpackage.internal.StandardBundlerParam.*;
       
    44 
       
    45 import static jdk.jpackage.internal.WindowsBundlerParam.*;
       
    46 
       
    47 /**
       
    48  * WinMsiBundler
       
    49  *
       
    50  * Produces .msi installer from application image. Uses WiX Toolkit to build
       
    51  * .msi installer.
       
    52  * <p>
       
    53  * {@link #execute} method creates a number of source files with the description
       
    54  * of installer to be processed by WiX tools. Generated source files are stored
       
    55  * in "config" subdirectory next to "app" subdirectory in the root work
       
    56  * directory. The following WiX source files are generated:
       
    57  * <ul>
       
    58  * <li>main.wxs. Main source file with the installer description
       
    59  * <li>bundle.wxf. Source file with application and Java run-time directory tree
       
    60  * description.
       
    61  * </ul>
       
    62  * <p>
       
    63  * main.wxs file is a copy of main.wxs resource from
       
    64  * jdk.jpackage.internal.resources package. It is parametrized with the
       
    65  * following WiX variables:
       
    66  * <ul>
       
    67  * <li>JpAppName. Name of the application. Set to the value of --name command
       
    68  * line option
       
    69  * <li>JpAppVersion. Version of the application. Set to the value of
       
    70  * --app-version command line option
       
    71  * <li>JpAppVendor. Vendor of the application. Set to the value of --vendor
       
    72  * command line option
       
    73  * <li>JpAppDescription. Description of the application. Set to the value of
       
    74  * --description command line option
       
    75  * <li>JpProductCode. Set to product code UUID of the application. Random value
       
    76  * generated by jpackage every time {@link #execute} method is called
       
    77  * <li>JpProductUpgradeCode. Set to upgrade code UUID of the application. Random
       
    78  * value generated by jpackage every time {@link #execute} method is called if
       
    79  * --win-upgrade-uuid command line option is not specified. Otherwise this
       
    80  * variable is set to the value of --win-upgrade-uuid command line option
       
    81  * <li>JpAllowDowngrades. Set to "yes" if --win-upgrade-uuid command line option
       
    82  * was specified. Undefined otherwise
       
    83  * <li>JpLicenseRtf. Set to the value of --license-file command line option.
       
    84  * Undefined is --license-file command line option was not specified
       
    85  * <li>JpInstallDirChooser. Set to "yes" if --win-dir-chooser command line
       
    86  * option was specified. Undefined otherwise
       
    87  * <li>JpConfigDir. Absolute path to the directory with generated WiX source
       
    88  * files.
       
    89  * <li>JpIsSystemWide. Set to "yes" if --win-per-user-install command line
       
    90  * option was not specified. Undefined otherwise
       
    91  * </ul>
       
    92  */
       
    93 public class WinMsiBundler  extends AbstractBundler {
       
    94 
       
    95     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER =
       
    96             new WindowsBundlerParam<>(
       
    97             "win.app.bundler",
       
    98             WinAppBundler.class,
       
    99             params -> new WinAppBundler(),
       
   100             null);
       
   101 
       
   102     public static final BundlerParamInfo<File> MSI_IMAGE_DIR =
       
   103             new WindowsBundlerParam<>(
       
   104             "win.msi.imageDir",
       
   105             File.class,
       
   106             params -> {
       
   107                 File imagesRoot = IMAGES_ROOT.fetchFrom(params);
       
   108                 if (!imagesRoot.exists()) imagesRoot.mkdirs();
       
   109                 return new File(imagesRoot, "win-msi.image");
       
   110             },
       
   111             (s, p) -> null);
       
   112 
       
   113     public static final BundlerParamInfo<File> WIN_APP_IMAGE =
       
   114             new WindowsBundlerParam<>(
       
   115             "win.app.image",
       
   116             File.class,
       
   117             null,
       
   118             (s, p) -> null);
       
   119 
       
   120     public static final StandardBundlerParam<Boolean> MSI_SYSTEM_WIDE  =
       
   121             new StandardBundlerParam<>(
       
   122                     Arguments.CLIOptions.WIN_PER_USER_INSTALLATION.getId(),
       
   123                     Boolean.class,
       
   124                     params -> true, // MSIs default to system wide
       
   125                     // valueOf(null) is false,
       
   126                     // and we actually do want null
       
   127                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null
       
   128                             : Boolean.valueOf(s)
       
   129             );
       
   130 
       
   131 
       
   132     public static final StandardBundlerParam<String> PRODUCT_VERSION =
       
   133             new StandardBundlerParam<>(
       
   134                     "win.msi.productVersion",
       
   135                     String.class,
       
   136                     VERSION::fetchFrom,
       
   137                     (s, p) -> s
       
   138             );
       
   139 
       
   140     private static final BundlerParamInfo<String> UPGRADE_UUID =
       
   141             new WindowsBundlerParam<>(
       
   142             Arguments.CLIOptions.WIN_UPGRADE_UUID.getId(),
       
   143             String.class,
       
   144             null,
       
   145             (s, p) -> s);
       
   146 
       
   147     @Override
       
   148     public String getName() {
       
   149         return I18N.getString("msi.bundler.name");
       
   150     }
       
   151 
       
   152     @Override
       
   153     public String getID() {
       
   154         return "msi";
       
   155     }
       
   156 
       
   157     @Override
       
   158     public String getBundleType() {
       
   159         return "INSTALLER";
       
   160     }
       
   161 
       
   162     @Override
       
   163     public File execute(Map<String, ? super Object> params,
       
   164             File outputParentDir) throws PackagerException {
       
   165         return bundle(params, outputParentDir);
       
   166     }
       
   167 
       
   168     @Override
       
   169     public boolean supported(boolean platformInstaller) {
       
   170         try {
       
   171             if (wixToolset == null) {
       
   172                 wixToolset = WixTool.toolset();
       
   173             }
       
   174             return true;
       
   175         } catch (ConfigException ce) {
       
   176             Log.error(ce.getMessage());
       
   177             if (ce.getAdvice() != null) {
       
   178                 Log.error(ce.getAdvice());
       
   179             }
       
   180         } catch (Exception e) {
       
   181             Log.error(e.getMessage());
       
   182         }
       
   183         return false;
       
   184     }
       
   185 
       
   186     @Override
       
   187     public boolean isDefault() {
       
   188         return false;
       
   189     }
       
   190 
       
   191     private static UUID getUpgradeCode(Map<String, ? super Object> params) {
       
   192         String upgradeCode = UPGRADE_UUID.fetchFrom(params);
       
   193         if (upgradeCode != null) {
       
   194             return UUID.fromString(upgradeCode);
       
   195         }
       
   196         return createNameUUID("UpgradeCode", params, List.of(VENDOR, APP_NAME));
       
   197     }
       
   198 
       
   199     private static UUID getProductCode(Map<String, ? super Object> params) {
       
   200         return createNameUUID("ProductCode", params, List.of(VENDOR, APP_NAME,
       
   201                 VERSION));
       
   202     }
       
   203 
       
   204     private static UUID createNameUUID(String prefix,
       
   205             Map<String, ? super Object> params,
       
   206             List<StandardBundlerParam<String>> components) {
       
   207         String key = Stream.concat(Stream.of(prefix), components.stream().map(
       
   208                 c -> c.fetchFrom(params))).collect(Collectors.joining("/"));
       
   209         return UUID.nameUUIDFromBytes(key.getBytes(StandardCharsets.UTF_8));
       
   210     }
       
   211 
       
   212     @Override
       
   213     public boolean validate(Map<String, ? super Object> params)
       
   214             throws ConfigException {
       
   215         try {
       
   216             if (wixToolset == null) {
       
   217                 wixToolset = WixTool.toolset();
       
   218             }
       
   219 
       
   220             try {
       
   221                 getUpgradeCode(params);
       
   222             } catch (IllegalArgumentException ex) {
       
   223                 throw new ConfigException(ex);
       
   224             }
       
   225 
       
   226             for (var toolInfo: wixToolset.values()) {
       
   227                 Log.verbose(MessageFormat.format(I18N.getString(
       
   228                         "message.tool-version"), toolInfo.path.getFileName(),
       
   229                         toolInfo.version));
       
   230             }
       
   231 
       
   232             wixSourcesBuilder.setWixVersion(wixToolset.get(WixTool.Light).version);
       
   233 
       
   234             wixSourcesBuilder.logWixFeatures();
       
   235 
       
   236             /********* validate bundle parameters *************/
       
   237 
       
   238             String version = PRODUCT_VERSION.fetchFrom(params);
       
   239             if (!isVersionStringValid(version)) {
       
   240                 throw new ConfigException(
       
   241                         MessageFormat.format(I18N.getString(
       
   242                                 "error.version-string-wrong-format"), version),
       
   243                         MessageFormat.format(I18N.getString(
       
   244                                 "error.version-string-wrong-format.advice"),
       
   245                                 PRODUCT_VERSION.getID()));
       
   246             }
       
   247 
       
   248             // only one mime type per association, at least one file extension
       
   249             List<Map<String, ? super Object>> associations =
       
   250                     FILE_ASSOCIATIONS.fetchFrom(params);
       
   251             if (associations != null) {
       
   252                 for (int i = 0; i < associations.size(); i++) {
       
   253                     Map<String, ? super Object> assoc = associations.get(i);
       
   254                     List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
       
   255                     if (mimes.size() > 1) {
       
   256                         throw new ConfigException(MessageFormat.format(
       
   257                                 I18N.getString("error.too-many-content-types-for-file-association"), i),
       
   258                                 I18N.getString("error.too-many-content-types-for-file-association.advice"));
       
   259                     }
       
   260                 }
       
   261             }
       
   262 
       
   263             return true;
       
   264         } catch (RuntimeException re) {
       
   265             if (re.getCause() instanceof ConfigException) {
       
   266                 throw (ConfigException) re.getCause();
       
   267             } else {
       
   268                 throw new ConfigException(re);
       
   269             }
       
   270         }
       
   271     }
       
   272 
       
   273     // https://msdn.microsoft.com/en-us/library/aa370859%28v=VS.85%29.aspx
       
   274     // The format of the string is as follows:
       
   275     //     major.minor.build
       
   276     // The first field is the major version and has a maximum value of 255.
       
   277     // The second field is the minor version and has a maximum value of 255.
       
   278     // The third field is called the build version or the update version and
       
   279     // has a maximum value of 65,535.
       
   280     static boolean isVersionStringValid(String v) {
       
   281         if (v == null) {
       
   282             return true;
       
   283         }
       
   284 
       
   285         String p[] = v.split("\\.");
       
   286         if (p.length > 3) {
       
   287             Log.verbose(I18N.getString(
       
   288                     "message.version-string-too-many-components"));
       
   289             return false;
       
   290         }
       
   291 
       
   292         try {
       
   293             int val = Integer.parseInt(p[0]);
       
   294             if (val < 0 || val > 255) {
       
   295                 Log.verbose(I18N.getString(
       
   296                         "error.version-string-major-out-of-range"));
       
   297                 return false;
       
   298             }
       
   299             if (p.length > 1) {
       
   300                 val = Integer.parseInt(p[1]);
       
   301                 if (val < 0 || val > 255) {
       
   302                     Log.verbose(I18N.getString(
       
   303                             "error.version-string-minor-out-of-range"));
       
   304                     return false;
       
   305                 }
       
   306             }
       
   307             if (p.length > 2) {
       
   308                 val = Integer.parseInt(p[2]);
       
   309                 if (val < 0 || val > 65535) {
       
   310                     Log.verbose(I18N.getString(
       
   311                             "error.version-string-build-out-of-range"));
       
   312                     return false;
       
   313                 }
       
   314             }
       
   315         } catch (NumberFormatException ne) {
       
   316             Log.verbose(I18N.getString("error.version-string-part-not-number"));
       
   317             Log.verbose(ne);
       
   318             return false;
       
   319         }
       
   320 
       
   321         return true;
       
   322     }
       
   323 
       
   324     private void prepareProto(Map<String, ? super Object> params)
       
   325                 throws PackagerException, IOException {
       
   326         File appImage = StandardBundlerParam.getPredefinedAppImage(params);
       
   327         File appDir = null;
       
   328 
       
   329         // we either have an application image or need to build one
       
   330         if (appImage != null) {
       
   331             appDir = new File(MSI_IMAGE_DIR.fetchFrom(params),
       
   332                     APP_NAME.fetchFrom(params));
       
   333             // copy everything from appImage dir into appDir/name
       
   334             IOUtils.copyRecursive(appImage.toPath(), appDir.toPath());
       
   335         } else {
       
   336             appDir = APP_BUNDLER.fetchFrom(params).doBundle(params,
       
   337                     MSI_IMAGE_DIR.fetchFrom(params), true);
       
   338         }
       
   339 
       
   340         params.put(WIN_APP_IMAGE.getID(), appDir);
       
   341 
       
   342         String licenseFile = LICENSE_FILE.fetchFrom(params);
       
   343         if (licenseFile != null) {
       
   344             // need to copy license file to the working directory
       
   345             // and convert to rtf if needed
       
   346             File lfile = new File(licenseFile);
       
   347             File destFile = new File(CONFIG_ROOT.fetchFrom(params),
       
   348                     lfile.getName());
       
   349 
       
   350             IOUtils.copyFile(lfile, destFile);
       
   351             destFile.setWritable(true);
       
   352             ensureByMutationFileIsRTF(destFile);
       
   353         }
       
   354     }
       
   355 
       
   356     public File bundle(Map<String, ? super Object> params, File outdir)
       
   357             throws PackagerException {
       
   358 
       
   359         IOUtils.writableOutputDir(outdir.toPath());
       
   360 
       
   361         Path imageDir = MSI_IMAGE_DIR.fetchFrom(params).toPath();
       
   362         try {
       
   363             Files.createDirectories(imageDir);
       
   364 
       
   365             prepareProto(params);
       
   366 
       
   367             wixSourcesBuilder
       
   368             .initFromParams(WIN_APP_IMAGE.fetchFrom(params).toPath(), params)
       
   369             .createMainFragment(CONFIG_ROOT.fetchFrom(params).toPath().resolve(
       
   370                     "bundle.wxf"));
       
   371 
       
   372             Map<String, String> wixVars = prepareMainProjectFile(params);
       
   373 
       
   374             new ScriptRunner()
       
   375             .setDirectory(imageDir)
       
   376             .setResourceCategoryId("resource.post-app-image-script")
       
   377             .setScriptNameSuffix("post-image")
       
   378             .setEnvironmentVariable("JpAppImageDir", imageDir.toAbsolutePath().toString())
       
   379             .run(params);
       
   380 
       
   381             return buildMSI(params, wixVars, outdir);
       
   382         } catch (IOException ex) {
       
   383             Log.verbose(ex);
       
   384             throw new PackagerException(ex);
       
   385         }
       
   386     }
       
   387 
       
   388     Map<String, String> prepareMainProjectFile(
       
   389             Map<String, ? super Object> params) throws IOException {
       
   390         Map<String, String> data = new HashMap<>();
       
   391 
       
   392         final UUID productCode = getProductCode(params);
       
   393         final UUID upgradeCode = getUpgradeCode(params);
       
   394 
       
   395         data.put("JpProductCode", productCode.toString());
       
   396         data.put("JpProductUpgradeCode", upgradeCode.toString());
       
   397 
       
   398         Log.verbose(MessageFormat.format(I18N.getString("message.product-code"),
       
   399                 productCode));
       
   400         Log.verbose(MessageFormat.format(I18N.getString("message.upgrade-code"),
       
   401                 upgradeCode));
       
   402 
       
   403         data.put("JpAllowUpgrades", "yes");
       
   404 
       
   405         data.put("JpAppName", APP_NAME.fetchFrom(params));
       
   406         data.put("JpAppDescription", DESCRIPTION.fetchFrom(params));
       
   407         data.put("JpAppVendor", VENDOR.fetchFrom(params));
       
   408         data.put("JpAppVersion", PRODUCT_VERSION.fetchFrom(params));
       
   409 
       
   410         final Path configDir = CONFIG_ROOT.fetchFrom(params).toPath();
       
   411 
       
   412         data.put("JpConfigDir", configDir.toAbsolutePath().toString());
       
   413 
       
   414         if (MSI_SYSTEM_WIDE.fetchFrom(params)) {
       
   415             data.put("JpIsSystemWide", "yes");
       
   416         }
       
   417 
       
   418         String licenseFile = LICENSE_FILE.fetchFrom(params);
       
   419         if (licenseFile != null) {
       
   420             String lname = new File(licenseFile).getName();
       
   421             File destFile = new File(CONFIG_ROOT.fetchFrom(params), lname);
       
   422             data.put("JpLicenseRtf", destFile.getAbsolutePath());
       
   423         }
       
   424 
       
   425         // Copy CA dll to include with installer
       
   426         if (INSTALLDIR_CHOOSER.fetchFrom(params)) {
       
   427             data.put("JpInstallDirChooser", "yes");
       
   428             String fname = "wixhelper.dll";
       
   429             try (InputStream is = OverridableResource.readDefault(fname)) {
       
   430                 Files.copy(is, Paths.get(
       
   431                         CONFIG_ROOT.fetchFrom(params).getAbsolutePath(),
       
   432                         fname));
       
   433             }
       
   434         }
       
   435 
       
   436         // Copy l10n files.
       
   437         for (String loc : Arrays.asList("en", "ja", "zh_CN")) {
       
   438             String fname = "MsiInstallerStrings_" + loc + ".wxl";
       
   439             try (InputStream is = OverridableResource.readDefault(fname)) {
       
   440                 Files.copy(is, Paths.get(
       
   441                         CONFIG_ROOT.fetchFrom(params).getAbsolutePath(),
       
   442                         fname));
       
   443             }
       
   444         }
       
   445 
       
   446         createResource("main.wxs", params)
       
   447                 .setCategory(I18N.getString("resource.main-wix-file"))
       
   448                 .saveToFile(configDir.resolve("main.wxs"));
       
   449 
       
   450         createResource("overrides.wxi", params)
       
   451                 .setCategory(I18N.getString("resource.overrides-wix-file"))
       
   452                 .saveToFile(configDir.resolve("overrides.wxi"));
       
   453 
       
   454         return data;
       
   455     }
       
   456 
       
   457     private File buildMSI(Map<String, ? super Object> params,
       
   458             Map<String, String> wixVars, File outdir)
       
   459             throws IOException {
       
   460 
       
   461         File msiOut = new File(
       
   462                 outdir, INSTALLER_FILE_NAME.fetchFrom(params) + ".msi");
       
   463 
       
   464         Log.verbose(MessageFormat.format(I18N.getString(
       
   465                 "message.preparing-msi-config"), msiOut.getAbsolutePath()));
       
   466 
       
   467         WixPipeline wixPipeline = new WixPipeline()
       
   468         .setToolset(wixToolset.entrySet().stream().collect(
       
   469                 Collectors.toMap(
       
   470                         entry -> entry.getKey(),
       
   471                         entry -> entry.getValue().path)))
       
   472         .setWixObjDir(TEMP_ROOT.fetchFrom(params).toPath().resolve("wixobj"))
       
   473         .setWorkDir(WIN_APP_IMAGE.fetchFrom(params).toPath())
       
   474         .addSource(CONFIG_ROOT.fetchFrom(params).toPath().resolve("main.wxs"), wixVars)
       
   475         .addSource(CONFIG_ROOT.fetchFrom(params).toPath().resolve("bundle.wxf"), null);
       
   476 
       
   477         Log.verbose(MessageFormat.format(I18N.getString(
       
   478                 "message.generating-msi"), msiOut.getAbsolutePath()));
       
   479 
       
   480         boolean enableLicenseUI = (LICENSE_FILE.fetchFrom(params) != null);
       
   481         boolean enableInstalldirUI = INSTALLDIR_CHOOSER.fetchFrom(params);
       
   482 
       
   483         List<String> lightArgs = new ArrayList<>();
       
   484 
       
   485         if (!MSI_SYSTEM_WIDE.fetchFrom(params)) {
       
   486             wixPipeline.addLightOptions("-sice:ICE91");
       
   487         }
       
   488         if (enableLicenseUI || enableInstalldirUI) {
       
   489             wixPipeline.addLightOptions("-ext", "WixUIExtension");
       
   490         }
       
   491 
       
   492         wixPipeline.addLightOptions("-loc",
       
   493                 CONFIG_ROOT.fetchFrom(params).toPath().resolve(I18N.getString(
       
   494                         "resource.wxl-file-name")).toAbsolutePath().toString());
       
   495 
       
   496         // Only needed if we using CA dll, so Wix can find it
       
   497         if (enableInstalldirUI) {
       
   498             wixPipeline.addLightOptions("-b", CONFIG_ROOT.fetchFrom(params).getAbsolutePath());
       
   499         }
       
   500 
       
   501         wixPipeline.buildMsi(msiOut.toPath().toAbsolutePath());
       
   502 
       
   503         return msiOut;
       
   504     }
       
   505 
       
   506     public static void ensureByMutationFileIsRTF(File f) {
       
   507         if (f == null || !f.isFile()) return;
       
   508 
       
   509         try {
       
   510             boolean existingLicenseIsRTF = false;
       
   511 
       
   512             try (FileInputStream fin = new FileInputStream(f)) {
       
   513                 byte[] firstBits = new byte[7];
       
   514 
       
   515                 if (fin.read(firstBits) == firstBits.length) {
       
   516                     String header = new String(firstBits);
       
   517                     existingLicenseIsRTF = "{\\rtf1\\".equals(header);
       
   518                 }
       
   519             }
       
   520 
       
   521             if (!existingLicenseIsRTF) {
       
   522                 List<String> oldLicense = Files.readAllLines(f.toPath());
       
   523                 try (Writer w = Files.newBufferedWriter(
       
   524                         f.toPath(), Charset.forName("Windows-1252"))) {
       
   525                     w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033"
       
   526                             + "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n"
       
   527                             + "\\viewkind4\\uc1\\pard\\sa200\\sl276"
       
   528                             + "\\slmult1\\lang9\\fs20 ");
       
   529                     oldLicense.forEach(l -> {
       
   530                         try {
       
   531                             for (char c : l.toCharArray()) {
       
   532                                 // 0x00 <= ch < 0x20 Escaped (\'hh)
       
   533                                 // 0x20 <= ch < 0x80 Raw(non - escaped) char
       
   534                                 // 0x80 <= ch <= 0xFF Escaped(\ 'hh)
       
   535                                 // 0x5C, 0x7B, 0x7D (special RTF characters
       
   536                                 // \,{,})Escaped(\'hh)
       
   537                                 // ch > 0xff Escaped (\\ud###?)
       
   538                                 if (c < 0x10) {
       
   539                                     w.write("\\'0");
       
   540                                     w.write(Integer.toHexString(c));
       
   541                                 } else if (c > 0xff) {
       
   542                                     w.write("\\ud");
       
   543                                     w.write(Integer.toString(c));
       
   544                                     // \\uc1 is in the header and in effect
       
   545                                     // so we trail with a replacement char if
       
   546                                     // the font lacks that character - '?'
       
   547                                     w.write("?");
       
   548                                 } else if ((c < 0x20) || (c >= 0x80) ||
       
   549                                         (c == 0x5C) || (c == 0x7B) ||
       
   550                                         (c == 0x7D)) {
       
   551                                     w.write("\\'");
       
   552                                     w.write(Integer.toHexString(c));
       
   553                                 } else {
       
   554                                     w.write(c);
       
   555                                 }
       
   556                             }
       
   557                             // blank lines are interpreted as paragraph breaks
       
   558                             if (l.length() < 1) {
       
   559                                 w.write("\\par");
       
   560                             } else {
       
   561                                 w.write(" ");
       
   562                             }
       
   563                             w.write("\r\n");
       
   564                         } catch (IOException e) {
       
   565                             Log.verbose(e);
       
   566                         }
       
   567                     });
       
   568                     w.write("}\r\n");
       
   569                 }
       
   570             }
       
   571         } catch (IOException e) {
       
   572             Log.verbose(e);
       
   573         }
       
   574 
       
   575     }
       
   576 
       
   577     private Map<WixTool, WixTool.ToolInfo> wixToolset;
       
   578     private WixSourcesBuilder wixSourcesBuilder = new WixSourcesBuilder();
       
   579 
       
   580 }