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.sun.javafx.tools.packager.bundlers.BundleParams;
  30 
  31 import java.io.*;
  32 import java.text.MessageFormat;
  33 import java.util.*;
  34 import java.util.regex.Matcher;
  35 import java.util.regex.Pattern;
  36 
  37 import static com.oracle.tools.packager.StandardBundlerParam.SECONDARY_LAUNCHERS;
  38 import static com.oracle.tools.packager.StandardBundlerParam.SERVICE_HINT;
  39 import static com.oracle.tools.packager.StandardBundlerParam.VERBOSE;
  40 import static com.oracle.tools.packager.windows.WindowsBundlerParam.*;
  41 
  42 public class WinExeBundler extends AbstractBundler {
  43 
  44     private static final ResourceBundle I18N =
  45             ResourceBundle.getBundle(WinExeBundler.class.getName());
  46     
  47     public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER = new WindowsBundlerParam<>(
  48             I18N.getString("param.app-bundler.name"),
  49             I18N.getString("param.app-bundler.description"),
  50             "win.app.bundler",
  51             WinAppBundler.class,
  52             params -> new WinAppBundler(),
  53             null);
  54 
  55     public static final BundlerParamInfo<WinServiceBundler> SERVICE_BUNDLER = new WindowsBundlerParam<>(
  56             I18N.getString("param.service-bundler.name"),
  57             I18N.getString("param.service-bundler.description"),
  58             "win.service.bundler",
  59             WinServiceBundler.class,
  60             params -> new WinServiceBundler(),
  61             null);
  62     
  63     public static final BundlerParamInfo<File> CONFIG_ROOT = new WindowsBundlerParam<>(
  64             I18N.getString("param.config-root.name"),
  65             I18N.getString("param.config-root.description"),
  66             "configRoot",
  67             File.class, params -> {
  68                 File imagesRoot = new File(BUILD_ROOT.fetchFrom(params), "windows");
  69                 imagesRoot.mkdirs();
  70                 return imagesRoot;
  71             },
  72             (s, p) -> null);
  73 
  74     //default for .exe is user level installation
  75     // only do system wide if explicitly requested
  76     public static final StandardBundlerParam<Boolean> EXE_SYSTEM_WIDE  =
  77             new StandardBundlerParam<>(
  78                     I18N.getString("param.system-wide.name"),
  79                     I18N.getString("param.system-wide.description"),
  80                     "win.exe." + BundleParams.PARAM_SYSTEM_WIDE,
  81                     Boolean.class,
  82                     params -> params.containsKey(SYSTEM_WIDE.getID())
  83                                 ? SYSTEM_WIDE.fetchFrom(params)
  84                                 : false, // EXEs default to user local install
  85                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s) // valueOf(null) is false, and we actually do want null
  86             );
  87 
  88     public static final BundlerParamInfo<File> EXE_IMAGE_DIR = new WindowsBundlerParam<>(
  89             I18N.getString("param.image-dir.name"),
  90             I18N.getString("param.image-dir.description"),
  91             "win.exe.imageDir",
  92             File.class,
  93             params -> {
  94                 File imagesRoot = IMAGES_ROOT.fetchFrom(params);
  95                 if (!imagesRoot.exists()) imagesRoot.mkdirs();
  96                 return new File(imagesRoot, "win-exe.image");
  97             },
  98             (s, p) -> null);
  99 
 100     private final static String DEFAULT_EXE_PROJECT_TEMPLATE = "template.iss";
 101     private static final String TOOL_INNO_SETUP_COMPILER = "iscc.exe";
 102 
 103     public static final BundlerParamInfo<String> TOOL_INNO_SETUP_COMPILER_EXECUTABLE = new WindowsBundlerParam<>(
 104             I18N.getString("param.iscc-path.name"),
 105             I18N.getString("param.iscc-path.description"),
 106             "win.exe.iscc.exe",
 107             String.class,
 108             params -> {
 109                 for (String dirString : (System.getenv("PATH") + ";C:\\Program Files (x86)\\Inno Setup 5;C:\\Program Files\\Inno Setup 5").split(";")) {
 110                     File f = new File(dirString.replace("\"", ""), TOOL_INNO_SETUP_COMPILER);
 111                     if (f.isFile()) {
 112                         return f.toString();
 113                     }
 114                 }
 115                 return null;
 116             },
 117             null);
 118 
 119     public WinExeBundler() {
 120         super();
 121         baseResourceLoader = WinResources.class;
 122     }
 123 
 124     @Override
 125     public String getName() {
 126         return I18N.getString("bundler.name");
 127     }
 128 
 129     @Override
 130     public String getDescription() {
 131         return I18N.getString("bundler.description");
 132     }
 133 
 134     @Override
 135     public String getID() {
 136         return "exe";
 137     }
 138 
 139     @Override
 140     public String getBundleType() {
 141         return "INSTALLER";
 142     }
 143 
 144     @Override
 145     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 146         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 147         results.addAll(WinAppBundler.getAppBundleParameters());
 148         results.addAll(getExeBundleParameters());
 149         return results;
 150     }
 151 
 152     public static Collection<BundlerParamInfo<?>> getExeBundleParameters() {
 153         return Arrays.asList(
 154                 DESCRIPTION,
 155                 COPYRIGHT,
 156                 LICENSE_FILE,
 157                 MENU_GROUP,
 158                 MENU_HINT,
 159 //                RUN_AT_STARTUP,
 160                 SHORTCUT_HINT,
 161 //                SERVICE_HINT,
 162 //                START_ON_INSTALL,
 163 //                STOP_ON_UNINSTALL,
 164                 SYSTEM_WIDE,
 165                 TITLE,
 166                 VENDOR,
 167                 INSTALLDIR_CHOOSER
 168         );
 169     }
 170 
 171     @Override
 172     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 173         return bundle(params, outputParentDir);
 174     }
 175 
 176     static class VersionExtractor extends PrintStream {
 177         double version = 0f;
 178 
 179         public VersionExtractor() {
 180             super(new ByteArrayOutputStream());
 181         }
 182 
 183         double getVersion() {
 184             if (version == 0f) {
 185                 String content = new String(((ByteArrayOutputStream) out).toByteArray());
 186                 Pattern pattern = Pattern.compile("Inno Setup (\\d+.?\\d*)");
 187                 Matcher matcher = pattern.matcher(content);
 188                 if (matcher.find()) {
 189                     String v = matcher.group(1);
 190                     version = new Double(v);
 191                 }
 192             }
 193             return version;
 194         }
 195     }
 196 
 197     private static double findToolVersion(String toolName) {
 198         try {
 199             if (toolName == null || "".equals(toolName)) return 0f;
 200 
 201             ProcessBuilder pb = new ProcessBuilder(
 202                     toolName,
 203                     "/?");
 204             VersionExtractor ve = new VersionExtractor();
 205             IOUtils.exec(pb, Log.isDebug(), true, ve); //not interested in the output
 206             double version = ve.getVersion();
 207             Log.verbose(MessageFormat.format(I18N.getString("message.tool-version"), toolName, version));
 208             return version;
 209         } catch (Exception e) {
 210             if (Log.isDebug()) {
 211                 e.printStackTrace();
 212             }
 213             return 0f;
 214         }
 215     }
 216 
 217     @Override
 218     public boolean validate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 219         try {
 220             if (p == null) throw new ConfigException(I18N.getString("error.parameters-null"), I18N.getString("error.parameters-null.advice"));
 221 
 222             //run basic validation to ensure requirements are met
 223             //we are not interested in return code, only possible exception
 224             APP_BUNDLER.fetchFrom(p).validate(p);
 225 
 226             // make sure some key values don't have newlines
 227             for (BundlerParamInfo<String> pi : Arrays.asList(
 228                     APP_NAME,
 229                     COPYRIGHT,
 230                     DESCRIPTION,
 231                     MENU_GROUP,
 232                     TITLE,
 233                     VENDOR,
 234                     VERSION)
 235             ) {
 236                 String v = pi.fetchFrom(p);
 237                 if (v.contains("\n") | v.contains("\r")) {
 238                     throw new ConfigException("Parmeter '" + pi.getID() + "' cannot contain a newline.",
 239                             "Change the value of '" + pi.getID() + " so that it does not contain any newlines");
 240                 }
 241             }
 242 
 243             //exe bundlers trim the copyright to 100 characters, tell them this will happen
 244             if (COPYRIGHT.fetchFrom(p).length() > 100) {
 245                 throw new ConfigException(
 246                         I18N.getString("error.copyright-is-too-long"),
 247                         I18N.getString("error.copyright-is-too-long.advice"));
 248             }
 249 
 250             // validate license file, if used, exists in the proper place
 251             if (p.containsKey(LICENSE_FILE.getID())) {
 252                 List<RelativeFileSet> appResourcesList = APP_RESOURCES_LIST.fetchFrom(p);
 253                 for (String license : LICENSE_FILE.fetchFrom(p)) {
 254                     boolean found = false;
 255                     for (RelativeFileSet appResources : appResourcesList) {
 256                         found = found || appResources.contains(license);
 257                     }
 258                     if (!found) {
 259                         throw new ConfigException(
 260                                 I18N.getString("error.license-missing"),
 261                                 MessageFormat.format(I18N.getString("error.license-missing.advice"),
 262                                         license));
 263                     }
 264                 }
 265             }
 266 
 267             if (SERVICE_HINT.fetchFrom(p)) {
 268                 SERVICE_BUNDLER.fetchFrom(p).validate(p);
 269             }
 270 
 271             double innoVersion = findToolVersion(TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p));
 272 
 273             //Inno Setup 5+ is required
 274             double minVersion = 5.0f;
 275 
 276             if (innoVersion < minVersion) {
 277                 Log.info(MessageFormat.format(I18N.getString("message.tool-wrong-version"), TOOL_INNO_SETUP_COMPILER, innoVersion, minVersion));
 278                 throw new ConfigException(
 279                         I18N.getString("error.iscc-not-found"),
 280                         I18N.getString("error.iscc-not-found.advice"));
 281             }
 282 
 283             return true;
 284         } catch (RuntimeException re) {
 285             if (re.getCause() instanceof ConfigException) {
 286                 throw (ConfigException) re.getCause();
 287             } else {
 288                 throw new ConfigException(re);
 289             }
 290         }
 291     }
 292 
 293     private boolean prepareProto(Map<String, ? super Object> params) throws IOException {
 294         File imageDir = EXE_IMAGE_DIR.fetchFrom(params);
 295         File appOutputDir = APP_BUNDLER.fetchFrom(params).doBundle(params, imageDir, true);
 296         if (appOutputDir == null) {
 297             return false;
 298         }
 299         
 300         List<String> licenseFiles = LICENSE_FILE.fetchFrom(params);
 301         if (licenseFiles != null) {
 302             //need to copy license file to the root of win.app.image
 303             outerLoop:
 304             for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(params)) {
 305                 for (String s : licenseFiles) {
 306                     if (rfs.contains(s)) {
 307                         File lfile = new File(rfs.getBaseDirectory(), s);
 308                         IOUtils.copyFile(lfile, new File(imageDir, lfile.getName()));
 309                         break outerLoop;
 310                     }
 311                 }
 312             }
 313         }
 314 
 315         for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
 316             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO
 317             if (icon != null && icon.exists()) {
 318                 IOUtils.copyFile(icon, new File(appOutputDir, icon.getName()));
 319             }
 320         }
 321         
 322         if (SERVICE_HINT.fetchFrom(params)) {
 323             // copies the service launcher to the app root folder
 324             appOutputDir = SERVICE_BUNDLER.fetchFrom(params).doBundle(params, appOutputDir, true);
 325             if (appOutputDir == null) {
 326                 return false;
 327             }
 328         }
 329         return true;
 330     }
 331 
 332     public File bundle(Map<String, ? super Object> p, File outputDirectory) {
 333         if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) {
 334             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath()));
 335         }
 336         if (!outputDirectory.canWrite()) {
 337             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath()));
 338         }
 339 
 340         // validate we have valid tools before continuing
 341         String iscc = TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p);
 342         if (iscc == null || !new File(iscc).isFile()) {
 343             Log.info(I18N.getString("error.iscc-not-found"));
 344             Log.info(MessageFormat.format(I18N.getString("message.iscc-file-string"), iscc));
 345             return null;
 346         }
 347 
 348         File imageDir = EXE_IMAGE_DIR.fetchFrom(p);
 349         try {
 350             imageDir.mkdirs();
 351 
 352             boolean menuShortcut = MENU_HINT.fetchFrom(p);
 353             boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p);
 354             if (!menuShortcut && !desktopShortcut) {
 355                 //both can not be false - user will not find the app
 356                 Log.verbose(I18N.getString("message.one-shortcut-required"));
 357                 p.put(MENU_HINT.getID(), true);
 358             }
 359 
 360             if (prepareProto(p) && prepareProjectConfig(p)) {
 361                 File configScript = getConfig_Script(p);
 362                 if (configScript.exists()) {
 363                     Log.info(MessageFormat.format(I18N.getString("message.running-wsh-script"), configScript.getAbsolutePath()));
 364                     IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p));
 365                 }
 366                 return buildEXE(p, outputDirectory);
 367             }
 368             return null;
 369         } catch (IOException ex) {
 370             ex.printStackTrace();
 371             return null;
 372         } finally {
 373             try {
 374                 if (VERBOSE.fetchFrom(p)) {
 375                     saveConfigFiles(p);
 376                 }
 377                 if (imageDir != null && !Log.isDebug()) {
 378                     IOUtils.deleteRecursive(imageDir);
 379                 } else if (imageDir != null) {
 380                     Log.info(MessageFormat.format(I18N.getString("message.debug-working-directory"), imageDir.getAbsolutePath()));
 381                 }
 382             } catch (FileNotFoundException ex) {
 383                 //noinspection ReturnInsideFinallyBlock
 384                 return null;
 385             }
 386         }
 387     }
 388 
 389     //name of post-image script
 390     private File getConfig_Script(Map<String, ? super Object> params) {
 391         return new File(EXE_IMAGE_DIR.fetchFrom(params), APP_NAME.fetchFrom(params) + "-post-image.wsf");
 392     }
 393 
 394     protected void saveConfigFiles(Map<String, ? super Object> params) {
 395         try {
 396             File configRoot = CONFIG_ROOT.fetchFrom(params);
 397             if (getConfig_ExeProjectFile(params).exists()) {
 398                 IOUtils.copyFile(getConfig_ExeProjectFile(params),
 399                         new File(configRoot, getConfig_ExeProjectFile(params).getName()));
 400             }
 401             if (getConfig_Script(params).exists()) {
 402                 IOUtils.copyFile(getConfig_Script(params),
 403                         new File(configRoot, getConfig_Script(params).getName()));
 404             }
 405             if (getConfig_SmallInnoSetupIcon(params).exists()) {
 406                 IOUtils.copyFile(getConfig_SmallInnoSetupIcon(params),
 407                         new File(configRoot, getConfig_SmallInnoSetupIcon(params).getName()));
 408             }
 409             Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), configRoot.getAbsolutePath()));
 410         } catch (IOException ioe) {
 411             ioe.printStackTrace();
 412         }
 413     }
 414 
 415     private String getAppIdentifier(Map<String, ? super Object> params) {
 416         String nm = IDENTIFIER.fetchFrom(params);
 417 
 418         //limitation of innosetup
 419         if (nm.length() > 126)
 420             nm = nm.substring(0, 126);
 421 
 422         return nm;
 423     }
 424 
 425 
 426     private String getLicenseFile(Map<String, ? super Object> params) {
 427         List<String> licenseFiles = LICENSE_FILE.fetchFrom(params);
 428         if (licenseFiles == null || licenseFiles.isEmpty()) {
 429             return "";
 430         } else {
 431             return licenseFiles.get(0);
 432         }
 433     }
 434 
 435     void validateValueAndPut(Map<String, String> data, String key, BundlerParamInfo<String> param, Map<String, ? super Object> params) throws IOException {
 436         String value = param.fetchFrom(params);
 437         if (value.contains("\r") || value.contains("\n")) {
 438             throw new IOException("Configuration Parameter " + param.getID() + " cannot contain multiple lines of text");
 439         }
 440         data.put(key, innosetupEscape(value));
 441     }
 442 
 443     private String innosetupEscape(String value) {
 444         if (value.contains("\"") || !value.trim().equals(value)) {
 445             value = "\"" + value.replace("\"", "\"\"") + "\"";
 446         }
 447         return value;
 448     }
 449 
 450     boolean prepareMainProjectFile(Map<String, ? super Object> params) throws IOException {
 451         Map<String, String> data = new HashMap<>();
 452         data.put("PRODUCT_APP_IDENTIFIER", innosetupEscape(getAppIdentifier(params)));
 453 
 454         validateValueAndPut(data, "APPLICATION_NAME", APP_NAME, params);
 455 
 456         validateValueAndPut(data, "APPLICATION_VENDOR", VENDOR, params);
 457         validateValueAndPut(data, "APPLICATION_VERSION", VERSION, params); // TODO make our own version paraminfo?
 458         validateValueAndPut(data, "INSTALLER_FILE_NAME", INSTALLER_FILE_NAME, params);
 459 
 460         data.put("APPLICATION_LAUNCHER_FILENAME", innosetupEscape(WinAppBundler.getLauncherName(params)));
 461 
 462         data.put("APPLICATION_DESKTOP_SHORTCUT", SHORTCUT_HINT.fetchFrom(params) ? "returnTrue" : "returnFalse");
 463         data.put("APPLICATION_MENU_SHORTCUT", MENU_HINT.fetchFrom(params) ? "returnTrue" : "returnFalse");
 464         validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, params);
 465         validateValueAndPut(data, "APPLICATION_COMMENTS", TITLE, params); // TODO this seems strange, at least in name
 466         validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, params);
 467 
 468         data.put("APPLICATION_LICENSE_FILE", innosetupEscape(getLicenseFile(params)));
 469         data.put("DISABLE_DIR_PAGE", INSTALLDIR_CHOOSER.fetchFrom(params) ? "No" : "Yes");
 470 
 471         Boolean isSystemWide = EXE_SYSTEM_WIDE.fetchFrom(params);
 472         
 473         if (isSystemWide) {
 474             data.put("APPLICATION_INSTALL_ROOT", "{pf}");
 475             data.put("APPLICATION_INSTALL_PRIVILEGE", "admin");
 476         } else {
 477             data.put("APPLICATION_INSTALL_ROOT", "{localappdata}");
 478             data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest");
 479         }
 480 
 481         if (BIT_ARCH_64.fetchFrom(params)) {
 482             data.put("ARCHITECTURE_BIT_MODE", "x64");
 483         } else {
 484             data.put("ARCHITECTURE_BIT_MODE", "");
 485         }
 486 
 487         if (SERVICE_HINT.fetchFrom(params)) {
 488             data.put("RUN_FILENAME", innosetupEscape(WinServiceBundler.getAppSvcName(params)));
 489         } else {
 490             validateValueAndPut(data, "RUN_FILENAME", APP_NAME, params);
 491         }
 492         validateValueAndPut(data, "APPLICATION_DESCRIPTION", DESCRIPTION, params);
 493         data.put("APPLICATION_SERVICE", SERVICE_HINT.fetchFrom(params) ? "returnTrue" : "returnFalse");
 494         data.put("APPLICATION_NOT_SERVICE", SERVICE_HINT.fetchFrom(params) ? "returnFalse" : "returnTrue");
 495         data.put("APPLICATION_APP_CDS", (UNLOCK_COMMERCIAL_FEATURES.fetchFrom(params) && ENABLE_APP_CDS.fetchFrom(params))
 496                 ? "returnTrue"
 497                 : "returnFalse");
 498         data.put("START_ON_INSTALL", START_ON_INSTALL.fetchFrom(params) ? "-startOnInstall" : "");
 499         data.put("STOP_ON_UNINSTALL", STOP_ON_UNINSTALL.fetchFrom(params) ? "-stopOnUninstall" : "");
 500         data.put("RUN_AT_STARTUP", RUN_AT_STARTUP.fetchFrom(params) ? "-runAtStartup" : "");
 501 
 502         StringBuilder secondaryLaunchersCfg = new StringBuilder();
 503         for (Map<String, ? super Object> launcher : SECONDARY_LAUNCHERS.fetchFrom(params)) {
 504             String application_name = APP_NAME.fetchFrom(launcher);
 505             if (MENU_HINT.fetchFrom(launcher)) {
 506                 //Name: "{group}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"
 507                 secondaryLaunchersCfg.append("Name: \"{group}\\");
 508                 secondaryLaunchersCfg.append(application_name);
 509                 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\");
 510                 secondaryLaunchersCfg.append(application_name);
 511                 secondaryLaunchersCfg.append(".exe\"; IconFilename: \"{app}\\");
 512                 secondaryLaunchersCfg.append(application_name);
 513                 secondaryLaunchersCfg.append(".ico\"\r\n");
 514             }
 515             if (SHORTCUT_HINT.fetchFrom(launcher)) {
 516                 //Name: "{commondesktop}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe";  IconFilename: "{app}\APPLICATION_NAME.ico"
 517                 secondaryLaunchersCfg.append("Name: \"{commondesktop}\\");
 518                 secondaryLaunchersCfg.append(application_name);
 519                 secondaryLaunchersCfg.append("\"; Filename: \"{app}\\");
 520                 secondaryLaunchersCfg.append(application_name);
 521                 secondaryLaunchersCfg.append(".exe\";  IconFilename: \"{app}\\");
 522                 secondaryLaunchersCfg.append(application_name);
 523                 secondaryLaunchersCfg.append(".ico\"\r\n");
 524             }
 525         }
 526         data.put("SECONDARY_LAUNCHERS", secondaryLaunchersCfg.toString());
 527 
 528         StringBuilder registryEntries = new StringBuilder();
 529         String regName = APP_REGISTRY_NAME.fetchFrom(params);
 530         List<Map<String, ? super Object>> fetchFrom = FILE_ASSOCIATIONS.fetchFrom(params);
 531         for (int i = 0; i < fetchFrom.size(); i++) {
 532             Map<String, ? super Object> fileAssociation = fetchFrom.get(i);
 533             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 534             File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICO
 535 
 536             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 537             String entryName = regName + "File";
 538             if (i > 0) {
 539                 entryName += "." + i;
 540             }
 541             
 542             if (extensions == null) {
 543                 Log.info(I18N.getString("message.creating-association-with-null-extension"));
 544             } else {
 545                 for (String ext : extensions) {
 546                     if (isSystemWide) {
 547                         // "Root: HKCR; Subkey: \".myp\"; ValueType: string; ValueName: \"\"; ValueData: \"MyProgramFile\"; Flags: uninsdeletevalue"
 548                         registryEntries.append("Root: HKCR; Subkey: \".")
 549                                 .append(ext)
 550                                 .append("\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 551                                 .append(entryName)
 552                                 .append("\"; Flags: uninsdeletevalue\r\n");
 553                     } else {
 554                         registryEntries.append("Root: HKCU; Subkey: \"Software\\Classes\\.")
 555                                 .append(ext)
 556                                 .append("\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 557                                 .append(entryName)
 558                                 .append("\"; Flags: uninsdeletevalue\r\n");
 559                     }
 560                 }
 561             }
 562 
 563             if (extensions != null && !extensions.isEmpty()) {
 564                 String ext = extensions.get(0);
 565                 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 566                 for (String mime : mimeTypes) {
 567                     if (isSystemWide) {
 568                         // "Root: HKCR; Subkey: HKCR\\Mime\\Database\\Content Type\\application/chaos; ValueType: string; ValueName: Extension; ValueData: .chaos; Flags: uninsdeletevalue"
 569                         registryEntries.append("Root: HKCR; Subkey: \"Mime\\Database\\Content Type\\")
 570                             .append(mime)
 571                             .append("\"; ValueType: string; ValueName: \"Extension\"; ValueData: \".")
 572                             .append(ext)
 573                             .append("\"; Flags: uninsdeletevalue\r\n");
 574                     } else {
 575                         registryEntries.append("Root: HKCU; Subkey: \"Software\\Classes\\Mime\\Database\\Content Type\\")
 576                                 .append(mime)
 577                                 .append("\"; ValueType: string; ValueName: \"Extension\"; ValueData: \".")
 578                                 .append(ext)
 579                                 .append("\"; Flags: uninsdeletevalue\r\n");
 580                     }
 581                 }
 582             }
 583 
 584             if (isSystemWide) {
 585                 //"Root: HKCR; Subkey: \"MyProgramFile\"; ValueType: string; ValueName: \"\"; ValueData: \"My Program File\"; Flags: uninsdeletekey"
 586                 registryEntries.append("Root: HKCR; Subkey: \"")
 587                     .append(entryName)
 588                     .append("\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 589                     .append(description)
 590                     .append("\"; Flags: uninsdeletekey\r\n");
 591             } else {
 592                 registryEntries.append("Root: HKCU; Subkey: \"Software\\Classes\\")
 593                     .append(entryName)
 594                     .append("\"; ValueType: string; ValueName: \"\"; ValueData: \"")
 595                     .append(description)
 596                     .append("\"; Flags: uninsdeletekey\r\n");
 597                 
 598             }
 599 
 600             if (icon != null && icon.exists()) {
 601                 if (isSystemWide) {
 602                     // "Root: HKCR; Subkey: \"MyProgramFile\\DefaultIcon\"; ValueType: string; ValueName: \"\"; ValueData: \"{app}\\MYPROG.EXE,0\"\n" +
 603                     registryEntries.append("Root: HKCR; Subkey: \"")
 604                         .append(entryName)
 605                         .append("\\DefaultIcon\"; ValueType: string; ValueName: \"\"; ValueData: \"{app}\\")
 606                         .append(icon.getName())
 607                         .append("\"\r\n");
 608                 } else {
 609                     registryEntries.append("Root: HKCU; Subkey: \"Software\\Classes\\")
 610                             .append(entryName)
 611                             .append("\\DefaultIcon\"; ValueType: string; ValueName: \"\"; ValueData: \"{app}\\")
 612                             .append(icon.getName())
 613                             .append("\"\r\n");
 614                 }
 615             }
 616 
 617             if (isSystemWide) {
 618                 //"Root: HKCR; Subkey: \"MyProgramFile\\shell\\open\\command\"; ValueType: string; ValueName: \"\"; ValueData: \"\"\"{app}\\MYPROG.EXE\"\" \"\"%1\"\"\"\n"
 619                 registryEntries.append("Root: HKCR; Subkey: \"")
 620                         .append(entryName)
 621                         .append("\\shell\\open\\command\"; ValueType: string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
 622                         .append(APP_NAME.fetchFrom(params))
 623                         .append("\"\" \"\"%1\"\"\"\r\n");
 624             } else {
 625                 registryEntries.append("Root: HKCU; Subkey: \"Software\\Classes\\")
 626                         .append(entryName)
 627                         .append("\\shell\\open\\command\"; ValueType: string; ValueName: \"\"; ValueData: \"\"\"{app}\\")
 628                         .append(APP_NAME.fetchFrom(params))
 629                         .append("\"\" \"\"%1\"\"\"\r\n");
 630             }
 631         }
 632         if (registryEntries.length() > 0) {
 633             data.put("FILE_ASSOCIATIONS", "ChangesAssociations=yes\r\n\r\n[Registry]\r\n" + registryEntries.toString());
 634         } else {
 635             data.put("FILE_ASSOCIATIONS", "");
 636         }
 637 
 638         Writer w = new BufferedWriter(new FileWriter(getConfig_ExeProjectFile(params)));
 639         String content = preprocessTextResource(
 640                 WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_ExeProjectFile(params).getName(),
 641                 I18N.getString("resource.inno-setup-project-file"), DEFAULT_EXE_PROJECT_TEMPLATE, data,
 642                 VERBOSE.fetchFrom(params),
 643                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 644         w.write(content);
 645         w.close();
 646         return true;
 647     }
 648 
 649     private final static String DEFAULT_INNO_SETUP_ICON = "icon_inno_setup.bmp";
 650 
 651     private boolean prepareProjectConfig(Map<String, ? super Object> params) throws IOException {
 652         prepareMainProjectFile(params);
 653 
 654         //prepare installer icon
 655         File iconTarget = getConfig_SmallInnoSetupIcon(params);
 656         fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + iconTarget.getName(),
 657                 I18N.getString("resource.setup-icon"),
 658                 DEFAULT_INNO_SETUP_ICON,
 659                 iconTarget,
 660                 VERBOSE.fetchFrom(params),
 661                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 662 
 663         fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_Script(params).getName(),
 664                 I18N.getString("resource.post-install-script"),
 665                 (String) null,
 666                 getConfig_Script(params),
 667                 VERBOSE.fetchFrom(params),
 668                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 669         return true;
 670     }
 671 
 672     private File getConfig_SmallInnoSetupIcon(Map<String, ? super Object> params) {
 673         return new File(EXE_IMAGE_DIR.fetchFrom(params),
 674                 APP_NAME.fetchFrom(params) + "-setup-icon.bmp");
 675     }
 676 
 677     private File getConfig_ExeProjectFile(Map<String, ? super Object> params) {
 678         return new File(EXE_IMAGE_DIR.fetchFrom(params),
 679                 APP_NAME.fetchFrom(params) + ".iss");
 680     }
 681 
 682 
 683     private File buildEXE(Map<String, ? super Object> params, File outdir) throws IOException {
 684         Log.verbose(MessageFormat.format(I18N.getString("message.outputting-to-location"), outdir.getAbsolutePath()));
 685 
 686         outdir.mkdirs();
 687 
 688         //run candle
 689         ProcessBuilder pb = new ProcessBuilder(
 690                 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(params),
 691                 "/o"+outdir.getAbsolutePath(),
 692                 getConfig_ExeProjectFile(params).getAbsolutePath());
 693         pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(params));
 694         IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 695 
 696         Log.info(MessageFormat.format(I18N.getString("message.output-location"), outdir.getAbsolutePath()));
 697 
 698         // presume the result is the ".exe" file with the newest modified time
 699         // not the best solution, but it is the most reliable
 700         File result = null;
 701         long lastModified = 0;
 702         File[] list = outdir.listFiles();
 703         if (list != null) {
 704             for (File f : list) {
 705                 if (f.getName().endsWith(".exe") && f.lastModified() > lastModified) {
 706                     result = f;
 707                     lastModified = f.lastModified();
 708                 }
 709             }
 710         }
 711 
 712         return result;
 713     }
 714 }