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