1 /* 2 * Copyright (c) 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 package jdk.jpackage.internal; 26 27 import java.awt.image.BufferedImage; 28 import java.io.BufferedReader; 29 import java.io.BufferedWriter; 30 import java.io.File; 31 import java.io.FileWriter; 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.io.InputStreamReader; 35 import java.io.Writer; 36 import java.nio.file.Files; 37 import java.nio.file.Path; 38 import java.text.MessageFormat; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.Collections; 42 import java.util.HashMap; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.ResourceBundle; 46 import java.util.stream.Collectors; 47 import java.util.stream.Stream; 48 import javax.imageio.ImageIO; 49 import javax.xml.stream.XMLOutputFactory; 50 import javax.xml.stream.XMLStreamException; 51 import javax.xml.stream.XMLStreamWriter; 52 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR; 53 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES; 54 import static jdk.jpackage.internal.LinuxAppImageBuilder.DEFAULT_ICON; 55 import static jdk.jpackage.internal.LinuxAppImageBuilder.ICON_PNG; 56 import static jdk.jpackage.internal.StandardBundlerParam.*; 57 58 59 abstract class LinuxPackageBundler extends AbstractBundler { 60 61 protected static final ResourceBundle I18N = ResourceBundle.getBundle( 62 "jdk.jpackage.internal.resources.LinuxResources"); 63 64 private static final String DESKTOP_COMMANDS_INSTALL = "DESKTOP_COMMANDS_INSTALL"; 65 private static final String DESKTOP_COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL"; 66 private static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS"; 67 68 private static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER = 69 new StandardBundlerParam<>( 70 "linux.app.bundler", 71 LinuxAppBundler.class, 72 (params) -> new LinuxAppBundler(), 73 null 74 ); 75 76 private static final BundlerParamInfo<String> MENU_GROUP = 77 new StandardBundlerParam<>( 78 Arguments.CLIOptions.LINUX_MENU_GROUP.getId(), 79 String.class, 80 params -> I18N.getString("param.menu-group.default"), 81 (s, p) -> s 82 ); 83 84 private static final StandardBundlerParam<Boolean> SHORTCUT_HINT = 85 new StandardBundlerParam<>( 86 Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId(), 87 Boolean.class, 88 params -> false, 89 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) 90 ? false : Boolean.valueOf(s) 91 ); 92 93 LinuxPackageBundler(BundlerParamInfo<String> packageName) { 94 this.packageName = packageName; 95 } 96 97 private final BundlerParamInfo<String> packageName; 98 99 @Override 100 final public boolean validate(Map<String, ? super Object> params) 101 throws ConfigException { 102 try { 103 if (params == null) throw new ConfigException( 104 I18N.getString("error.parameters-null"), 105 I18N.getString("error.parameters-null.advice")); 106 107 // run basic validation to ensure requirements are met 108 // we are not interested in return code, only possible exception 109 APP_BUNDLER.fetchFrom(params).validate(params); 110 111 validateFileAssociations(FILE_ASSOCIATIONS.fetchFrom(params)); 112 113 // If package name has some restrictions, the string converter will 114 // throw an exception if invalid 115 packageName.getStringConverter().apply(packageName.fetchFrom(params), 116 params); 117 118 // Packaging specific validation 119 doValidate(params); 120 121 return true; 122 } catch (RuntimeException re) { 123 if (re.getCause() instanceof ConfigException) { 124 throw (ConfigException) re.getCause(); 125 } else { 126 throw new ConfigException(re); 127 } 128 } 129 } 130 131 @Override 132 final public String getBundleType() { 133 return "INSTALLER"; 134 } 135 136 @Override 137 final public File execute(Map<String, ? super Object> params, 138 File outputParentDir) throws PackagerException { 139 IOUtils.writableOutputDir(outputParentDir.toPath()); 140 141 PlatformPackage thePackage = createMetaPackage(params); 142 143 try { 144 File appImage = StandardBundlerParam.getPredefinedAppImage(params); 145 146 // we either have an application image or need to build one 147 if (appImage != null) { 148 appImageLayout(params).resolveAt(appImage.toPath()).copy( 149 thePackage.sourceApplicationLayout()); 150 } else { 151 appImage = APP_BUNDLER.fetchFrom(params).doBundle(params, 152 thePackage.sourceRoot().toFile(), true); 153 ApplicationLayout srcAppLayout = appImageLayout(params).resolveAt( 154 appImage.toPath()); 155 if (appImage.equals(PREDEFINED_RUNTIME_IMAGE.fetchFrom(params))) { 156 // Application image points to run-time image. 157 // Copy it. 158 srcAppLayout.copy(thePackage.sourceApplicationLayout()); 159 } else { 160 // Application image is a newly created directory tree. 161 // Move it. 162 srcAppLayout.move(thePackage.sourceApplicationLayout()); 163 if (appImage.exists()) { 164 // Empty app image directory might remain after all application 165 // directories have been moved. 166 appImage.delete(); 167 } 168 } 169 } 170 171 Map<String, String> data = createDefaultReplacementData(params); 172 if (StandardBundlerParam.isRuntimeInstaller(params)) { 173 Stream.of(DESKTOP_COMMANDS_INSTALL, DESKTOP_COMMANDS_UNINSTALL, 174 UTILITY_SCRIPTS).forEach(v -> data.put(v, "")); 175 } else { 176 data.putAll( 177 new DesktopIntegration(thePackage, params).prepareForApplication()); 178 } 179 180 data.putAll(createReplacementData(params)); 181 182 return buildPackageBundle(Collections.unmodifiableMap(data), params, 183 outputParentDir); 184 } catch (IOException ex) { 185 Log.verbose(ex); 186 throw new PackagerException(ex); 187 } 188 } 189 190 private Map<String, String> createDefaultReplacementData( 191 Map<String, ? super Object> params) throws IOException { 192 Map<String, String> data = new HashMap<>(); 193 194 data.put("APPLICATION_PACKAGE", createMetaPackage(params).name()); 195 data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params)); 196 data.put("APPLICATION_VERSION", VERSION.fetchFrom(params)); 197 data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params)); 198 data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params)); 199 data.put("PACKAGE_DEPENDENCIES", LINUX_PACKAGE_DEPENDENCIES.fetchFrom( 200 params)); 201 202 return data; 203 } 204 205 abstract void doValidate(Map<String, ? super Object> params) 206 throws ConfigException; 207 208 abstract protected Map<String, String> createReplacementData( 209 Map<String, ? super Object> params) throws IOException; 210 211 abstract protected File buildPackageBundle( 212 Map<String, String> replacementData, 213 Map<String, ? super Object> params, File outputParentDir) throws 214 PackagerException, IOException; 215 216 final protected PlatformPackage createMetaPackage( 217 Map<String, ? super Object> params) { 218 return new PlatformPackage() { 219 @Override 220 public String name() { 221 return packageName.fetchFrom(params); 222 } 223 224 @Override 225 public Path sourceRoot() { 226 return IMAGES_ROOT.fetchFrom(params).toPath().toAbsolutePath(); 227 } 228 229 @Override 230 public ApplicationLayout sourceApplicationLayout() { 231 return appImageLayout(params).resolveAt( 232 applicationInstallDir(sourceRoot())); 233 } 234 235 @Override 236 public ApplicationLayout installedApplicationLayout() { 237 return appImageLayout(params).resolveAt( 238 applicationInstallDir(Path.of("/"))); 239 } 240 241 private Path applicationInstallDir(Path root) { 242 Path installDir = Path.of(LINUX_INSTALL_DIR.fetchFrom(params), 243 name()); 244 if (installDir.isAbsolute()) { 245 installDir = Path.of("." + installDir.toString()).normalize(); 246 } 247 return root.resolve(installDir); 248 } 249 }; 250 } 251 252 private ApplicationLayout appImageLayout( 253 Map<String, ? super Object> params) { 254 if (StandardBundlerParam.isRuntimeInstaller(params)) { 255 return ApplicationLayout.javaRuntime(); 256 } 257 return ApplicationLayout.unixApp(); 258 } 259 260 private static void validateFileAssociations( 261 List<Map<String, ? super Object>> associations) throws 262 ConfigException { 263 // only one mime type per association, at least one file extention 264 int assocIdx = 0; 265 for (var assoc : associations) { 266 ++assocIdx; 267 List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc); 268 if (mimes == null || mimes.isEmpty()) { 269 String msgKey = "error.no-content-types-for-file-association"; 270 throw new ConfigException( 271 MessageFormat.format(I18N.getString(msgKey), assocIdx), 272 I18N.getString(msgKey + ".advise")); 273 274 } 275 276 if (mimes.size() > 1) { 277 String msgKey = "error.too-many-content-types-for-file-association"; 278 throw new ConfigException( 279 MessageFormat.format(I18N.getString(msgKey), assocIdx), 280 I18N.getString(msgKey + ".advise")); 281 } 282 } 283 } 284 285 /** 286 * Helper to create files for desktop integration. 287 */ 288 private class DesktopIntegration { 289 290 DesktopIntegration(PlatformPackage thePackage, 291 Map<String, ? super Object> params) { 292 293 associations = FILE_ASSOCIATIONS.fetchFrom(params).stream().filter( 294 a -> { 295 if (a == null) { 296 return false; 297 } 298 List<String> mimes = FA_CONTENT_TYPE.fetchFrom(a); 299 return (mimes != null && !mimes.isEmpty()); 300 }).collect(Collectors.toUnmodifiableList()); 301 302 launchers = ADD_LAUNCHERS.fetchFrom(params); 303 304 this.thePackage = thePackage; 305 306 customIconFile = ICON_PNG.fetchFrom(params); 307 308 verbose = VERBOSE.fetchFrom(params); 309 resourceDir = RESOURCE_DIR.fetchFrom(params); 310 311 // XDG recommends to use vendor prefix in desktop file names as xdg 312 // commands copy files to system directories. 313 // Package name should be a good prefix. 314 final String desktopFileName = String.format("%s-%s.desktop", 315 thePackage.name(), APP_NAME.fetchFrom(params)); 316 final String mimeInfoFileName = String.format("%s-%s-MimeInfo.xml", 317 thePackage.name(), APP_NAME.fetchFrom(params)); 318 319 mimeInfoFile = new DesktopFile(mimeInfoFileName); 320 321 if (!associations.isEmpty() || SHORTCUT_HINT.fetchFrom(params) || customIconFile != null) { 322 // 323 // Create primary .desktop file if one of conditions is met: 324 // - there are file associations configured 325 // - user explicitely requested to create a shortcut 326 // - custom icon specified 327 // 328 desktopFile = new DesktopFile(desktopFileName); 329 iconFile = new DesktopFile(String.format("%s.png", 330 APP_NAME.fetchFrom(params))); 331 } else { 332 desktopFile = null; 333 iconFile = null; 334 } 335 336 this.desktopFileData = Collections.unmodifiableMap( 337 createDataForDesktopFile(params)); 338 } 339 340 Map<String, String> prepareForApplication() throws IOException { 341 if (iconFile != null) { 342 // Create application icon file. 343 prepareSrcIconFile(); 344 } 345 346 Map<String, String> data = new HashMap<>(desktopFileData); 347 348 final ShellCommands shellCommands; 349 if (desktopFile != null) { 350 // Create application desktop description file. 351 createDesktopFile(data); 352 353 // Shell commands will be created only if desktop file 354 // should be installed. 355 shellCommands = new ShellCommands(); 356 } else { 357 shellCommands = null; 358 } 359 360 if (!associations.isEmpty()) { 361 // Create XML file with mime types corresponding to file associations. 362 createFileAssociationsMimeInfoFile(); 363 364 shellCommands.setFileAssociations(); 365 366 // Create icon files corresponding to file associations 367 Map<String, Path> mimeTypeWithIconFile = createFileAssociationIconFiles(); 368 mimeTypeWithIconFile.forEach((k, v) -> { 369 shellCommands.addIcon(k, v); 370 }); 371 } 372 373 // Create shell commands to install/uninstall integration with desktop of the app. 374 if (shellCommands != null) { 375 shellCommands.applyTo(data); 376 } 377 378 boolean needCleanupScripts = !associations.isEmpty(); 379 380 // Take care of additional launchers if there are any. 381 // Process every additional launcher as the main application launcher. 382 // Collect shell commands to install/uninstall integration with desktop 383 // of the additional launchers and append them to the corresponding 384 // commands of the main launcher. 385 List<String> installShellCmds = new ArrayList<>(Arrays.asList( 386 data.get(DESKTOP_COMMANDS_INSTALL))); 387 List<String> uninstallShellCmds = new ArrayList<>(Arrays.asList( 388 data.get(DESKTOP_COMMANDS_UNINSTALL))); 389 for (Map<String, ? super Object> params : launchers) { 390 DesktopIntegration integration = new DesktopIntegration( 391 thePackage, params); 392 393 if (!integration.associations.isEmpty()) { 394 needCleanupScripts = true; 395 } 396 397 Map<String, String> launcherData = integration.prepareForApplication(); 398 399 installShellCmds.add(launcherData.get(DESKTOP_COMMANDS_INSTALL)); 400 uninstallShellCmds.add(launcherData.get( 401 DESKTOP_COMMANDS_UNINSTALL)); 402 } 403 404 data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands( 405 installShellCmds)); 406 data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands( 407 uninstallShellCmds)); 408 409 if (needCleanupScripts) { 410 // Pull in utils.sh scrips library. 411 try (InputStream is = getResourceAsStream("utils.sh"); 412 InputStreamReader isr = new InputStreamReader(is); 413 BufferedReader reader = new BufferedReader(isr)) { 414 data.put(UTILITY_SCRIPTS, reader.lines().collect( 415 Collectors.joining(System.lineSeparator()))); 416 } 417 } else { 418 data.put(UTILITY_SCRIPTS, ""); 419 } 420 421 return data; 422 } 423 424 private Map<String, String> createDataForDesktopFile( 425 Map<String, ? super Object> params) { 426 Map<String, String> data = new HashMap<>(); 427 data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params)); 428 data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params)); 429 data.put("APPLICATION_ICON", 430 iconFile != null ? iconFile.installPath().toString() : null); 431 data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params)); 432 data.put("APPLICATION_LAUNCHER", 433 thePackage.installedApplicationLayout().launchersDirectory().resolve( 434 LinuxAppImageBuilder.getLauncherName(params)).toString()); 435 436 return data; 437 } 438 439 /** 440 * Shell commands to integrate something with desktop. 441 */ 442 private class ShellCommands { 443 444 ShellCommands() { 445 registerIconCmds = new ArrayList<>(); 446 unregisterIconCmds = new ArrayList<>(); 447 448 registerDesktopFileCmd = String.join(" ", "xdg-desktop-menu", 449 "install", desktopFile.installPath().toString()); 450 unregisterDesktopFileCmd = String.join(" ", "xdg-desktop-menu", 451 "uninstall", desktopFile.installPath().toString()); 452 } 453 454 void setFileAssociations() { 455 registerFileAssociationsCmd = String.join(" ", "xdg-mime", 456 "install", 457 mimeInfoFile.installPath().toString()); 458 unregisterFileAssociationsCmd = String.join(" ", "xdg-mime", 459 "uninstall", mimeInfoFile.installPath().toString()); 460 461 // 462 // Add manual cleanup of system files to get rid of 463 // the default mime type handlers. 464 // 465 // Even after mime type is unregisterd with `xdg-mime uninstall` 466 // command and desktop file deleted with `xdg-desktop-menu uninstall` 467 // command, records in 468 // `/usr/share/applications/defaults.list` (Ubuntu 16) or 469 // `/usr/local/share/applications/defaults.list` (OracleLinux 7) 470 // files remain referencing deleted mime time and deleted 471 // desktop file which makes `xdg-mime query default` output name 472 // of non-existing desktop file. 473 // 474 String cleanUpCommand = String.join(" ", 475 "uninstall_default_mime_handler", 476 desktopFile.installPath().getFileName().toString(), 477 String.join(" ", getMimeTypeNamesFromFileAssociations())); 478 479 unregisterFileAssociationsCmd = stringifyShellCommands( 480 unregisterFileAssociationsCmd, cleanUpCommand); 481 } 482 483 void addIcon(String mimeType, Path iconFile) { 484 final int imgSize = getSquareSizeOfImage(iconFile.toFile()); 485 final String dashMime = mimeType.replace('/', '-'); 486 registerIconCmds.add(String.join(" ", "xdg-icon-resource", 487 "install", "--context", "mimetypes", "--size ", 488 Integer.toString(imgSize), iconFile.toString(), dashMime)); 489 unregisterIconCmds.add(String.join(" ", "xdg-icon-resource", 490 "uninstall", dashMime)); 491 } 492 493 void applyTo(Map<String, String> data) { 494 List<String> cmds = new ArrayList<>(); 495 496 cmds.add(registerDesktopFileCmd); 497 cmds.add(registerFileAssociationsCmd); 498 cmds.addAll(registerIconCmds); 499 data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands(cmds)); 500 501 cmds.clear(); 502 cmds.add(unregisterDesktopFileCmd); 503 cmds.add(unregisterFileAssociationsCmd); 504 cmds.addAll(unregisterIconCmds); 505 data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands(cmds)); 506 } 507 508 private String registerDesktopFileCmd; 509 private String unregisterDesktopFileCmd; 510 511 private String registerFileAssociationsCmd; 512 private String unregisterFileAssociationsCmd; 513 514 private List<String> registerIconCmds; 515 private List<String> unregisterIconCmds; 516 } 517 518 private final PlatformPackage thePackage; 519 520 private final List<Map<String, ? super Object>> associations; 521 522 private final List<Map<String, ? super Object>> launchers; 523 524 /** 525 * Desktop integration file. xml, icon, etc. 526 * Resides somewhere in application installation tree. 527 * Has two paths: 528 * - path where it should be placed at package build time; 529 * - path where it should be installed by package manager; 530 */ 531 private class DesktopFile { 532 533 DesktopFile(String fileName) { 534 installPath = thePackage 535 .installedApplicationLayout() 536 .destktopIntegrationDirectory().resolve(fileName); 537 srcPath = thePackage 538 .sourceApplicationLayout() 539 .destktopIntegrationDirectory().resolve(fileName); 540 } 541 542 private final Path installPath; 543 private final Path srcPath; 544 545 Path installPath() { 546 return installPath; 547 } 548 549 Path srcPath() { 550 return srcPath; 551 } 552 } 553 554 private final boolean verbose; 555 private final File resourceDir; 556 557 private final DesktopFile mimeInfoFile; 558 private final DesktopFile desktopFile; 559 private final DesktopFile iconFile; 560 561 private final Map<String, String> desktopFileData; 562 563 /** 564 * Path to icon file provided by user or null. 565 */ 566 private final File customIconFile; 567 568 private void appendFileAssociation(XMLStreamWriter xml, 569 Map<String, ? super Object> assoc) throws XMLStreamException { 570 571 xml.writeStartElement("mime-type"); 572 final String thisMime = FA_CONTENT_TYPE.fetchFrom(assoc).get(0); 573 xml.writeAttribute("type", thisMime); 574 575 final String description = FA_DESCRIPTION.fetchFrom(assoc); 576 if (description != null && !description.isEmpty()) { 577 xml.writeStartElement("comment"); 578 xml.writeCharacters(description); 579 xml.writeEndElement(); 580 } 581 582 final List<String> extensions = FA_EXTENSIONS.fetchFrom(assoc); 583 if (extensions == null) { 584 Log.error(I18N.getString( 585 "message.creating-association-with-null-extension")); 586 } else { 587 for (String ext : extensions) { 588 xml.writeStartElement("glob"); 589 xml.writeAttribute("pattern", "*." + ext); 590 xml.writeEndElement(); 591 } 592 } 593 594 xml.writeEndElement(); 595 } 596 597 private void createFileAssociationsMimeInfoFile() throws IOException { 598 XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance(); 599 600 try (Writer w = new BufferedWriter(new FileWriter( 601 mimeInfoFile.srcPath().toFile()))) { 602 XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w); 603 604 xml.writeStartDocument(); 605 xml.writeStartElement("mime-info"); 606 xml.writeNamespace("xmlns", 607 "http://www.freedesktop.org/standards/shared-mime-info"); 608 609 for (var assoc : associations) { 610 appendFileAssociation(xml, assoc); 611 } 612 613 xml.writeEndElement(); 614 xml.writeEndDocument(); 615 xml.flush(); 616 xml.close(); 617 618 } catch (XMLStreamException ex) { 619 Log.verbose(ex); 620 throw new IOException(ex); 621 } 622 } 623 624 private Map<String, Path> createFileAssociationIconFiles() throws 625 IOException { 626 Map<String, Path> mimeTypeWithIconFile = new HashMap<>(); 627 for (var assoc : associations) { 628 File customFaIcon = FA_ICON.fetchFrom(assoc); 629 if (customFaIcon == null || !customFaIcon.exists() || getSquareSizeOfImage( 630 customFaIcon) == 0) { 631 continue; 632 } 633 634 String fname = iconFile.srcPath().getFileName().toString(); 635 if (fname.indexOf(".") > 0) { 636 fname = fname.substring(0, fname.lastIndexOf(".")); 637 } 638 639 DesktopFile faIconFile = new DesktopFile( 640 fname + "_fa_" + customFaIcon.getName()); 641 642 IOUtils.copyFile(customFaIcon, faIconFile.srcPath().toFile()); 643 644 mimeTypeWithIconFile.put(FA_CONTENT_TYPE.fetchFrom(assoc).get(0), 645 faIconFile.installPath()); 646 } 647 return mimeTypeWithIconFile; 648 } 649 650 private void createDesktopFile(Map<String, String> data) throws IOException { 651 List<String> mimeTypes = getMimeTypeNamesFromFileAssociations(); 652 data.put("DESKTOP_MIMES", "MimeType=" + String.join(";", mimeTypes)); 653 654 // prepare desktop shortcut 655 try (Writer w = Files.newBufferedWriter(desktopFile.srcPath())) { 656 String content = preprocessTextResource( 657 desktopFile.srcPath().getFileName().toString(), 658 I18N.getString("resource.menu-shortcut-descriptor"), 659 "template.desktop", 660 data, 661 verbose, 662 resourceDir); 663 w.write(content); 664 } 665 } 666 667 private void prepareSrcIconFile() throws IOException { 668 if (customIconFile == null || !customIconFile.exists()) { 669 fetchResource(iconFile.srcPath().getFileName().toString(), 670 I18N.getString("resource.menu-icon"), 671 DEFAULT_ICON, 672 iconFile.srcPath().toFile(), 673 verbose, 674 resourceDir); 675 } else { 676 fetchResource(iconFile.srcPath().getFileName().toString(), 677 I18N.getString("resource.menu-icon"), 678 customIconFile, 679 iconFile.srcPath().toFile(), 680 verbose, 681 resourceDir); 682 } 683 } 684 685 private List<String> getMimeTypeNamesFromFileAssociations() { 686 return associations.stream().map( 687 a -> FA_CONTENT_TYPE.fetchFrom(a).get(0)).collect( 688 Collectors.toUnmodifiableList()); 689 } 690 } 691 692 private static int getSquareSizeOfImage(File f) { 693 try { 694 BufferedImage bi = ImageIO.read(f); 695 if (bi.getWidth() == bi.getHeight()) { 696 return bi.getWidth(); 697 } 698 } catch (IOException e) { 699 Log.verbose(e); 700 } 701 return 0; 702 } 703 704 private static String stringifyShellCommands(String ... commands) { 705 return stringifyShellCommands(Arrays.asList(commands)); 706 } 707 708 private static String stringifyShellCommands(List<String> commands) { 709 return String.join(System.lineSeparator(), commands.stream().filter( 710 s -> s != null && !s.isEmpty()).collect(Collectors.toList())); 711 } 712 }