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