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