< prev index next >

src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppImageBuilder.java

Print this page




  59 import static jdk.jpackage.internal.MacBaseInstallerBundler.*;
  60 import static jdk.jpackage.internal.MacAppBundler.*;
  61 
  62 public class MacAppImageBuilder extends AbstractAppImageBuilder {
  63 
  64     private static final ResourceBundle I18N = ResourceBundle.getBundle(
  65             "jdk.jpackage.internal.resources.MacResources");
  66 
  67     private static final String LIBRARY_NAME = "libapplauncher.dylib";
  68     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  69     private static final String OS_TYPE_CODE = "APPL";
  70     private static final String TEMPLATE_INFO_PLIST_LITE =
  71             "Info-lite.plist.template";
  72     private static final String TEMPLATE_RUNTIME_INFO_PLIST =
  73             "Runtime-Info.plist.template";
  74 
  75     private final Path root;
  76     private final Path contentsDir;
  77     private final Path javaDir;
  78     private final Path javaModsDir;
  79     private final String relativeModsDir;
  80     private final Path resourcesDir;
  81     private final Path macOSDir;
  82     private final Path runtimeDir;
  83     private final Path runtimeRoot;
  84     private final Path mdir;
  85 
  86     private final Map<String, ? super Object> params;
  87 
  88     private static List<String> keyChains;
  89 
  90     public static final BundlerParamInfo<Boolean>
  91             MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>(
  92                     "mac.configure-launcher-in-plist",
  93                     Boolean.class,
  94                     params -> Boolean.FALSE,
  95                     (s, p) -> Boolean.valueOf(s));
  96 
  97     public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
  98             new EnumeratedBundlerParam<>(
  99                     Arguments.CLIOptions.MAC_APP_STORE_CATEGORY.getId(),


 159             Arguments.CLIOptions.MAC_SIGN.getId(),
 160             Boolean.class,
 161             params -> false,
 162             // valueOf(null) is false, we actually do want null in some cases
 163             (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ?
 164                     null : Boolean.valueOf(s)
 165         );
 166 
 167     public MacAppImageBuilder(Map<String, Object> config, Path imageOutDir)
 168             throws IOException {
 169         super(config, imageOutDir.resolve(APP_NAME.fetchFrom(config)
 170                 + ".app/Contents/runtime/Contents/Home"));
 171 
 172         Objects.requireNonNull(imageOutDir);
 173 
 174         this.params = config;
 175         this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app");
 176         this.contentsDir = root.resolve("Contents");
 177         this.javaDir = contentsDir.resolve("Java");
 178         this.javaModsDir = javaDir.resolve("mods");
 179         this.relativeModsDir = "Java/mods";
 180         this.resourcesDir = contentsDir.resolve("Resources");
 181         this.macOSDir = contentsDir.resolve("MacOS");
 182         this.runtimeDir = contentsDir.resolve("runtime");
 183         this.runtimeRoot = runtimeDir.resolve("Contents/Home");
 184         this.mdir = runtimeRoot.resolve("lib");
 185         Files.createDirectories(javaDir);
 186         Files.createDirectories(resourcesDir);
 187         Files.createDirectories(macOSDir);
 188         Files.createDirectories(runtimeDir);
 189     }
 190 
 191     public MacAppImageBuilder(Map<String, Object> config, String jreName,
 192             Path imageOutDir) throws IOException {
 193         super(null, imageOutDir.resolve(jreName + "/Contents/Home"));
 194 
 195         Objects.requireNonNull(imageOutDir);
 196 
 197         this.params = config;
 198         this.root = imageOutDir.resolve(jreName );
 199         this.contentsDir = root.resolve("Contents");
 200         this.javaDir = null;
 201         this.javaModsDir = null;
 202         this.relativeModsDir = null;
 203         this.resourcesDir = null;
 204         this.macOSDir = null;
 205         this.runtimeDir = this.root;
 206         this.runtimeRoot = runtimeDir.resolve("Contents/Home");
 207         this.mdir = runtimeRoot.resolve("lib");
 208 
 209         Files.createDirectories(runtimeDir);
 210     }
 211 
 212     private void writeEntry(InputStream in, Path dstFile) throws IOException {
 213         Files.createDirectories(dstFile.getParent());
 214         Files.copy(in, dstFile);
 215     }
 216 
 217     public static boolean validCFBundleVersion(String v) {
 218         // CFBundleVersion (String - iOS, OS X) specifies the build version
 219         // number of the bundle, which identifies an iteration (released or
 220         // unreleased) of the bundle. The build version number should be a
 221         // string comprised of three non-negative, period-separated integers
 222         // with the first integer being greater than zero. The string should


 261         } catch (NumberFormatException ne) {
 262             Log.verbose(I18N.getString("message.version-string-numbers-only"));
 263             Log.verbose(ne);
 264             return false;
 265         }
 266 
 267         return true;
 268     }
 269 
 270     @Override
 271     public Path getAppDir() {
 272         return javaDir;
 273     }
 274 
 275     @Override
 276     public Path getAppModsDir() {
 277         return javaModsDir;
 278     }
 279 
 280     @Override
 281     public String getRelativeModsDir() {
 282         return relativeModsDir;
 283     }
 284 
 285     @Override
 286     public void prepareApplicationFiles() throws IOException {
 287         Map<String, ? super Object> originalParams = new HashMap<>(params);
 288         // Generate PkgInfo
 289         File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo");
 290         pkgInfoFile.createNewFile();
 291         writePkgInfo(pkgInfoFile);
 292 
 293         Path executable = macOSDir.resolve(getLauncherName(params));
 294 
 295         // create the main app launcher
 296         try (InputStream is_launcher =
 297                 getResourceAsStream("jpackageapplauncher");
 298             InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) {
 299             // Copy executable and library to MacOS folder
 300             writeEntry(is_launcher, executable);
 301             writeEntry(is_lib, macOSDir.resolve(LIBRARY_NAME));
 302         }
 303         executable.toFile().setExecutable(true, false);
 304         // generate main app launcher config file
 305         File cfg = new File(root.toFile(), getLauncherCfgName(params));
 306         writeCfgFile(params, cfg, "$APPDIR/runtime");
 307 
 308         // create additional app launcher(s) and config file(s)
 309         List<Map<String, ? super Object>> entryPoints =
 310                 StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params);
 311         for (Map<String, ? super Object> entryPoint : entryPoints) {
 312             Map<String, ? super Object> tmp =
 313                     AddLauncherArguments.merge(originalParams, entryPoint);
 314 
 315             // add executable for add launcher
 316             Path addExecutable = macOSDir.resolve(getLauncherName(tmp));
 317             try (InputStream is = getResourceAsStream("jpackageapplauncher");) {
 318                 writeEntry(is, addExecutable);
 319             }
 320             addExecutable.toFile().setExecutable(true, false);
 321 
 322             // add config file for add launcher
 323             cfg = new File(root.toFile(), getLauncherCfgName(tmp));
 324             writeCfgFile(tmp, cfg, "$APPDIR/runtime");
 325         }
 326 
 327         // Copy class path entries to Java folder
 328         copyClassPathEntries(javaDir);
 329 
 330         /*********** Take care of "config" files *******/
 331         File icon = ICON_ICNS.fetchFrom(params);
 332 
 333         InputStream in = locateResource(
 334                 APP_NAME.fetchFrom(params) + ".icns",
 335                 "icon",
 336                 DEFAULT_ICNS_ICON.fetchFrom(params),
 337                 icon,
 338                 VERBOSE.fetchFrom(params),
 339                 RESOURCE_DIR.fetchFrom(params));
 340         Files.copy(in,
 341                 resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns"),
 342                 StandardCopyOption.REPLACE_EXISTING);
 343 
 344         // copy file association icons


 472                     RESOURCE_DIR.fetchFrom(params)));
 473         }
 474     }
 475 
 476     private void writeInfoPlist(File file) throws IOException {
 477         Log.verbose(MessageFormat.format(I18N.getString(
 478                 "message.preparing-info-plist"), file.getAbsolutePath()));
 479 
 480         //prepare config for exe
 481         //Note: do not need CFBundleDisplayName if we don't support localization
 482         Map<String, String> data = new HashMap<>();
 483         data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns");
 484         data.put("DEPLOY_BUNDLE_IDENTIFIER",
 485                 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
 486         data.put("DEPLOY_BUNDLE_NAME",
 487                 getBundleName(params));
 488         data.put("DEPLOY_BUNDLE_COPYRIGHT",
 489                 COPYRIGHT.fetchFrom(params) != null ?
 490                 COPYRIGHT.fetchFrom(params) : "Unknown");
 491         data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
 492         data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/runtime");
 493         data.put("DEPLOY_BUNDLE_SHORT_VERSION",
 494                 VERSION.fetchFrom(params) != null ?
 495                 VERSION.fetchFrom(params) : "1.0.0");
 496         data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
 497                 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ?
 498                 MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
 499         data.put("DEPLOY_BUNDLE_CATEGORY", MAC_CATEGORY.fetchFrom(params));
 500 
 501         boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null;
 502         boolean hasMainModule =
 503                 StandardBundlerParam.MODULE.fetchFrom(params) != null;
 504 
 505         if (hasMainJar) {
 506             data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).
 507                     getIncludedFiles().iterator().next());
 508         }
 509         else if (hasMainModule) {
 510             data.put("DEPLOY_MODULE_NAME",
 511                     StandardBundlerParam.MODULE.fetchFrom(params));
 512         }


 522         }
 523 
 524         data.put("DEPLOY_JAVA_OPTIONS", sb.toString());
 525 
 526         sb = new StringBuilder();
 527         List<String> args = ARGUMENTS.fetchFrom(params);
 528         newline = "";
 529         // So we don't add unneccessary extra line after last append
 530 
 531         for (String o : args) {
 532             sb.append(newline).append("    <string>").append(o).append(
 533                     "</string>");
 534             newline = "\n";
 535         }
 536         data.put("DEPLOY_ARGUMENTS", sb.toString());
 537 
 538         newline = "";
 539 
 540         data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
 541 
 542         StringBuilder macroedPath = new StringBuilder();
 543         for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) {
 544             macroedPath.append(s);
 545             macroedPath.append(":");
 546         }
 547         macroedPath.deleteCharAt(macroedPath.length() - 1);
 548 
 549         data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString());
 550 
 551         StringBuilder bundleDocumentTypes = new StringBuilder();
 552         StringBuilder exportedTypes = new StringBuilder();
 553         for (Map<String, ? super Object>
 554                 fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
 555 
 556             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 557 
 558             if (extensions == null) {
 559                 Log.verbose(I18N.getString(
 560                         "message.creating-association-with-null-extension"));
 561             }
 562 
 563             List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 564             String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)
 565                     + "." + ((extensions == null || extensions.isEmpty())
 566                     ? "mime" : extensions.get(0));
 567             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 568             File icon = FA_ICON.fetchFrom(fileAssociation);
 569 




  59 import static jdk.jpackage.internal.MacBaseInstallerBundler.*;
  60 import static jdk.jpackage.internal.MacAppBundler.*;
  61 
  62 public class MacAppImageBuilder extends AbstractAppImageBuilder {
  63 
  64     private static final ResourceBundle I18N = ResourceBundle.getBundle(
  65             "jdk.jpackage.internal.resources.MacResources");
  66 
  67     private static final String LIBRARY_NAME = "libapplauncher.dylib";
  68     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  69     private static final String OS_TYPE_CODE = "APPL";
  70     private static final String TEMPLATE_INFO_PLIST_LITE =
  71             "Info-lite.plist.template";
  72     private static final String TEMPLATE_RUNTIME_INFO_PLIST =
  73             "Runtime-Info.plist.template";
  74 
  75     private final Path root;
  76     private final Path contentsDir;
  77     private final Path javaDir;
  78     private final Path javaModsDir;

  79     private final Path resourcesDir;
  80     private final Path macOSDir;
  81     private final Path runtimeDir;
  82     private final Path runtimeRoot;
  83     private final Path mdir;
  84 
  85     private final Map<String, ? super Object> params;
  86 
  87     private static List<String> keyChains;
  88 
  89     public static final BundlerParamInfo<Boolean>
  90             MAC_CONFIGURE_LAUNCHER_IN_PLIST = new StandardBundlerParam<>(
  91                     "mac.configure-launcher-in-plist",
  92                     Boolean.class,
  93                     params -> Boolean.FALSE,
  94                     (s, p) -> Boolean.valueOf(s));
  95 
  96     public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
  97             new EnumeratedBundlerParam<>(
  98                     Arguments.CLIOptions.MAC_APP_STORE_CATEGORY.getId(),


 158             Arguments.CLIOptions.MAC_SIGN.getId(),
 159             Boolean.class,
 160             params -> false,
 161             // valueOf(null) is false, we actually do want null in some cases
 162             (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ?
 163                     null : Boolean.valueOf(s)
 164         );
 165 
 166     public MacAppImageBuilder(Map<String, Object> config, Path imageOutDir)
 167             throws IOException {
 168         super(config, imageOutDir.resolve(APP_NAME.fetchFrom(config)
 169                 + ".app/Contents/runtime/Contents/Home"));
 170 
 171         Objects.requireNonNull(imageOutDir);
 172 
 173         this.params = config;
 174         this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params) + ".app");
 175         this.contentsDir = root.resolve("Contents");
 176         this.javaDir = contentsDir.resolve("Java");
 177         this.javaModsDir = javaDir.resolve("mods");

 178         this.resourcesDir = contentsDir.resolve("Resources");
 179         this.macOSDir = contentsDir.resolve("MacOS");
 180         this.runtimeDir = contentsDir.resolve("runtime");
 181         this.runtimeRoot = runtimeDir.resolve("Contents/Home");
 182         this.mdir = runtimeRoot.resolve("lib");
 183         Files.createDirectories(javaDir);
 184         Files.createDirectories(resourcesDir);
 185         Files.createDirectories(macOSDir);
 186         Files.createDirectories(runtimeDir);
 187     }
 188 
 189     public MacAppImageBuilder(Map<String, Object> config, String jreName,
 190             Path imageOutDir) throws IOException {
 191         super(null, imageOutDir.resolve(jreName + "/Contents/Home"));
 192 
 193         Objects.requireNonNull(imageOutDir);
 194 
 195         this.params = config;
 196         this.root = imageOutDir.resolve(jreName );
 197         this.contentsDir = root.resolve("Contents");
 198         this.javaDir = null;
 199         this.javaModsDir = null;

 200         this.resourcesDir = null;
 201         this.macOSDir = null;
 202         this.runtimeDir = this.root;
 203         this.runtimeRoot = runtimeDir.resolve("Contents/Home");
 204         this.mdir = runtimeRoot.resolve("lib");
 205 
 206         Files.createDirectories(runtimeDir);
 207     }
 208 
 209     private void writeEntry(InputStream in, Path dstFile) throws IOException {
 210         Files.createDirectories(dstFile.getParent());
 211         Files.copy(in, dstFile);
 212     }
 213 
 214     public static boolean validCFBundleVersion(String v) {
 215         // CFBundleVersion (String - iOS, OS X) specifies the build version
 216         // number of the bundle, which identifies an iteration (released or
 217         // unreleased) of the bundle. The build version number should be a
 218         // string comprised of three non-negative, period-separated integers
 219         // with the first integer being greater than zero. The string should


 258         } catch (NumberFormatException ne) {
 259             Log.verbose(I18N.getString("message.version-string-numbers-only"));
 260             Log.verbose(ne);
 261             return false;
 262         }
 263 
 264         return true;
 265     }
 266 
 267     @Override
 268     public Path getAppDir() {
 269         return javaDir;
 270     }
 271 
 272     @Override
 273     public Path getAppModsDir() {
 274         return javaModsDir;
 275     }
 276 
 277     @Override





 278     public void prepareApplicationFiles() throws IOException {
 279         Map<String, ? super Object> originalParams = new HashMap<>(params);
 280         // Generate PkgInfo
 281         File pkgInfoFile = new File(contentsDir.toFile(), "PkgInfo");
 282         pkgInfoFile.createNewFile();
 283         writePkgInfo(pkgInfoFile);
 284 
 285         Path executable = macOSDir.resolve(getLauncherName(params));
 286 
 287         // create the main app launcher
 288         try (InputStream is_launcher =
 289                 getResourceAsStream("jpackageapplauncher");
 290             InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) {
 291             // Copy executable and library to MacOS folder
 292             writeEntry(is_launcher, executable);
 293             writeEntry(is_lib, macOSDir.resolve(LIBRARY_NAME));
 294         }
 295         executable.toFile().setExecutable(true, false);
 296         // generate main app launcher config file
 297         File cfg = new File(root.toFile(), getLauncherCfgName(params));
 298         writeCfgFile(params, cfg);
 299 
 300         // create additional app launcher(s) and config file(s)
 301         List<Map<String, ? super Object>> entryPoints =
 302                 StandardBundlerParam.ADD_LAUNCHERS.fetchFrom(params);
 303         for (Map<String, ? super Object> entryPoint : entryPoints) {
 304             Map<String, ? super Object> tmp =
 305                     AddLauncherArguments.merge(originalParams, entryPoint);
 306 
 307             // add executable for add launcher
 308             Path addExecutable = macOSDir.resolve(getLauncherName(tmp));
 309             try (InputStream is = getResourceAsStream("jpackageapplauncher");) {
 310                 writeEntry(is, addExecutable);
 311             }
 312             addExecutable.toFile().setExecutable(true, false);
 313 
 314             // add config file for add launcher
 315             cfg = new File(root.toFile(), getLauncherCfgName(tmp));
 316             writeCfgFile(tmp, cfg);
 317         }
 318 
 319         // Copy class path entries to Java folder
 320         copyClassPathEntries(javaDir);
 321 
 322         /*********** Take care of "config" files *******/
 323         File icon = ICON_ICNS.fetchFrom(params);
 324 
 325         InputStream in = locateResource(
 326                 APP_NAME.fetchFrom(params) + ".icns",
 327                 "icon",
 328                 DEFAULT_ICNS_ICON.fetchFrom(params),
 329                 icon,
 330                 VERBOSE.fetchFrom(params),
 331                 RESOURCE_DIR.fetchFrom(params));
 332         Files.copy(in,
 333                 resourcesDir.resolve(APP_NAME.fetchFrom(params) + ".icns"),
 334                 StandardCopyOption.REPLACE_EXISTING);
 335 
 336         // copy file association icons


 464                     RESOURCE_DIR.fetchFrom(params)));
 465         }
 466     }
 467 
 468     private void writeInfoPlist(File file) throws IOException {
 469         Log.verbose(MessageFormat.format(I18N.getString(
 470                 "message.preparing-info-plist"), file.getAbsolutePath()));
 471 
 472         //prepare config for exe
 473         //Note: do not need CFBundleDisplayName if we don't support localization
 474         Map<String, String> data = new HashMap<>();
 475         data.put("DEPLOY_ICON_FILE", APP_NAME.fetchFrom(params) + ".icns");
 476         data.put("DEPLOY_BUNDLE_IDENTIFIER",
 477                 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params));
 478         data.put("DEPLOY_BUNDLE_NAME",
 479                 getBundleName(params));
 480         data.put("DEPLOY_BUNDLE_COPYRIGHT",
 481                 COPYRIGHT.fetchFrom(params) != null ?
 482                 COPYRIGHT.fetchFrom(params) : "Unknown");
 483         data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
 484         data.put("DEPLOY_JAVA_RUNTIME_NAME", getCfgRuntimeDir());
 485         data.put("DEPLOY_BUNDLE_SHORT_VERSION",
 486                 VERSION.fetchFrom(params) != null ?
 487                 VERSION.fetchFrom(params) : "1.0.0");
 488         data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION",
 489                 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ?
 490                 MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100");
 491         data.put("DEPLOY_BUNDLE_CATEGORY", MAC_CATEGORY.fetchFrom(params));
 492 
 493         boolean hasMainJar = MAIN_JAR.fetchFrom(params) != null;
 494         boolean hasMainModule =
 495                 StandardBundlerParam.MODULE.fetchFrom(params) != null;
 496 
 497         if (hasMainJar) {
 498             data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).
 499                     getIncludedFiles().iterator().next());
 500         }
 501         else if (hasMainModule) {
 502             data.put("DEPLOY_MODULE_NAME",
 503                     StandardBundlerParam.MODULE.fetchFrom(params));
 504         }


 514         }
 515 
 516         data.put("DEPLOY_JAVA_OPTIONS", sb.toString());
 517 
 518         sb = new StringBuilder();
 519         List<String> args = ARGUMENTS.fetchFrom(params);
 520         newline = "";
 521         // So we don't add unneccessary extra line after last append
 522 
 523         for (String o : args) {
 524             sb.append(newline).append("    <string>").append(o).append(
 525                     "</string>");
 526             newline = "\n";
 527         }
 528         data.put("DEPLOY_ARGUMENTS", sb.toString());
 529 
 530         newline = "";
 531 
 532         data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
 533 
 534         data.put("DEPLOY_APP_CLASSPATH",
 535                   getCfgClassPath(CLASSPATH.fetchFrom(params)));






 536 
 537         StringBuilder bundleDocumentTypes = new StringBuilder();
 538         StringBuilder exportedTypes = new StringBuilder();
 539         for (Map<String, ? super Object>
 540                 fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) {
 541 
 542             List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation);
 543 
 544             if (extensions == null) {
 545                 Log.verbose(I18N.getString(
 546                         "message.creating-association-with-null-extension"));
 547             }
 548 
 549             List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation);
 550             String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)
 551                     + "." + ((extensions == null || extensions.isEmpty())
 552                     ? "mime" : extensions.get(0));
 553             String description = FA_DESCRIPTION.fetchFrom(fileAssociation);
 554             File icon = FA_ICON.fetchFrom(fileAssociation);
 555 


< prev index next >