src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java
branchJDK-8200758-branch
changeset 58885 d1602ae35212
parent 58762 0fe62353385b
equal deleted inserted replaced
58818:a9316bb4c0e8 58885:d1602ae35212
    24  */
    24  */
    25 package jdk.jpackage.internal;
    25 package jdk.jpackage.internal;
    26 
    26 
    27 import java.awt.image.BufferedImage;
    27 import java.awt.image.BufferedImage;
    28 import java.io.*;
    28 import java.io.*;
       
    29 import java.nio.file.Files;
    29 import java.nio.file.Path;
    30 import java.nio.file.Path;
    30 import java.util.*;
    31 import java.util.*;
    31 import java.util.stream.Collectors;
    32 import java.util.stream.Collectors;
    32 import java.util.stream.Stream;
    33 import java.util.stream.Stream;
    33 import javax.imageio.ImageIO;
    34 import javax.imageio.ImageIO;
    48     static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS";
    49     static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS";
    49 
    50 
    50     DesktopIntegration(PlatformPackage thePackage,
    51     DesktopIntegration(PlatformPackage thePackage,
    51             Map<String, ? super Object> params) {
    52             Map<String, ? super Object> params) {
    52 
    53 
    53         associations = FILE_ASSOCIATIONS.fetchFrom(params).stream().filter(
    54         associations = FileAssociation.fetchFrom(params).stream()
    54                 a -> {
    55                 .filter(fa -> !fa.mimeTypes.isEmpty())
    55                     if (a == null) {
    56                 .map(LinuxFileAssociation::new)
    56                         return false;
    57                 .collect(Collectors.toUnmodifiableList());
    57                     }
       
    58                     List<String> mimes = FA_CONTENT_TYPE.fetchFrom(a);
       
    59                     return (mimes != null && !mimes.isEmpty());
       
    60                 }).collect(Collectors.toUnmodifiableList());
       
    61 
    58 
    62         launchers = ADD_LAUNCHERS.fetchFrom(params);
    59         launchers = ADD_LAUNCHERS.fetchFrom(params);
    63 
    60 
    64         this.thePackage = thePackage;
    61         this.thePackage = thePackage;
    65 
    62 
   110                 List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap(
   107                 List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap(
   111                 List::stream).distinct().collect(Collectors.toList());
   108                 List::stream).distinct().collect(Collectors.toList());
   112     }
   109     }
   113 
   110 
   114     Map<String, String> create() throws IOException {
   111     Map<String, String> create() throws IOException {
       
   112         associations.forEach(assoc -> assoc.data.verify());
       
   113 
   115         if (iconFile != null) {
   114         if (iconFile != null) {
   116             // Create application icon file.
   115             // Create application icon file.
   117             iconResource.saveToFile(iconFile.srcPath());
   116             iconResource.saveToFile(iconFile.srcPath());
   118         }
   117         }
   119 
   118 
   136             createFileAssociationsMimeInfoFile();
   135             createFileAssociationsMimeInfoFile();
   137 
   136 
   138             shellCommands.setFileAssociations();
   137             shellCommands.setFileAssociations();
   139 
   138 
   140             // Create icon files corresponding to file associations
   139             // Create icon files corresponding to file associations
   141             Map<String, Path> mimeTypeWithIconFile = createFileAssociationIconFiles();
   140             addFileAssociationIconFiles(shellCommands);
   142             mimeTypeWithIconFile.forEach((k, v) -> {
       
   143                 shellCommands.addIcon(k, v);
       
   144             });
       
   145         }
   141         }
   146 
   142 
   147         // Create shell commands to install/uninstall integration with desktop of the app.
   143         // Create shell commands to install/uninstall integration with desktop of the app.
   148         if (shellCommands != null) {
   144         if (shellCommands != null) {
   149             shellCommands.applyTo(data);
   145             shellCommands.applyTo(data);
   257             unregisterFileAssociationsCmd = stringifyShellCommands(
   253             unregisterFileAssociationsCmd = stringifyShellCommands(
   258                     unregisterFileAssociationsCmd, cleanUpCommand);
   254                     unregisterFileAssociationsCmd, cleanUpCommand);
   259         }
   255         }
   260 
   256 
   261         void addIcon(String mimeType, Path iconFile) {
   257         void addIcon(String mimeType, Path iconFile) {
   262             final int imgSize = getSquareSizeOfImage(iconFile.toFile());
   258             addIcon(mimeType, iconFile, getSquareSizeOfImage(iconFile.toFile()));
       
   259         }
       
   260 
       
   261         void addIcon(String mimeType, Path iconFile, int imgSize) {
       
   262             imgSize = normalizeIconSize(imgSize);
   263             final String dashMime = mimeType.replace('/', '-');
   263             final String dashMime = mimeType.replace('/', '-');
   264             registerIconCmds.add(String.join(" ", "xdg-icon-resource",
   264             registerIconCmds.add(String.join(" ", "xdg-icon-resource",
   265                     "install", "--context", "mimetypes", "--size ",
   265                     "install", "--context", "mimetypes", "--size",
   266                     Integer.toString(imgSize), iconFile.toString(), dashMime));
   266                     Integer.toString(imgSize), iconFile.toString(), dashMime));
   267             unregisterIconCmds.add(String.join(" ", "xdg-icon-resource",
   267             unregisterIconCmds.add(String.join(" ", "xdg-icon-resource",
   268                     "uninstall", dashMime));
   268                     "uninstall", dashMime, "--size", Integer.toString(imgSize)));
   269         }
   269         }
   270 
   270 
   271         void applyTo(Map<String, String> data) {
   271         void applyTo(Map<String, String> data) {
   272             List<String> cmds = new ArrayList<>();
   272             List<String> cmds = new ArrayList<>();
   273 
   273 
   322             return srcPath;
   322             return srcPath;
   323         }
   323         }
   324     }
   324     }
   325 
   325 
   326     private void appendFileAssociation(XMLStreamWriter xml,
   326     private void appendFileAssociation(XMLStreamWriter xml,
   327             Map<String, ? super Object> assoc) throws XMLStreamException {
   327             FileAssociation assoc) throws XMLStreamException {
   328 
   328 
   329         xml.writeStartElement("mime-type");
   329         for (var mimeType : assoc.mimeTypes) {
   330         final String thisMime = FA_CONTENT_TYPE.fetchFrom(assoc).get(0);
   330             xml.writeStartElement("mime-type");
   331         xml.writeAttribute("type", thisMime);
   331             xml.writeAttribute("type", mimeType);
   332 
   332 
   333         final String description = FA_DESCRIPTION.fetchFrom(assoc);
   333             final String description = assoc.description;
   334         if (description != null && !description.isEmpty()) {
   334             if (description != null && !description.isEmpty()) {
   335             xml.writeStartElement("comment");
   335                 xml.writeStartElement("comment");
   336             xml.writeCharacters(description);
   336                 xml.writeCharacters(description);
   337             xml.writeEndElement();
   337                 xml.writeEndElement();
   338         }
   338             }
   339 
   339 
   340         final List<String> extensions = FA_EXTENSIONS.fetchFrom(assoc);
   340             for (String ext : assoc.extensions) {
   341         if (extensions == null) {
       
   342             Log.error(I18N.getString(
       
   343                     "message.creating-association-with-null-extension"));
       
   344         } else {
       
   345             for (String ext : extensions) {
       
   346                 xml.writeStartElement("glob");
   341                 xml.writeStartElement("glob");
   347                 xml.writeAttribute("pattern", "*." + ext);
   342                 xml.writeAttribute("pattern", "*." + ext);
   348                 xml.writeEndElement();
   343                 xml.writeEndElement();
   349             }
   344             }
   350         }
   345 
   351 
   346             xml.writeEndElement();
   352         xml.writeEndElement();
   347         }
   353     }
   348     }
   354 
   349 
   355     private void createFileAssociationsMimeInfoFile() throws IOException {
   350     private void createFileAssociationsMimeInfoFile() throws IOException {
   356         IOUtils.createXml(mimeInfoFile.srcPath(), xml -> {
   351         IOUtils.createXml(mimeInfoFile.srcPath(), xml -> {
   357             xml.writeStartElement("mime-info");
   352             xml.writeStartElement("mime-info");
   358             xml.writeDefaultNamespace(
   353             xml.writeDefaultNamespace(
   359                     "http://www.freedesktop.org/standards/shared-mime-info");
   354                     "http://www.freedesktop.org/standards/shared-mime-info");
   360 
   355 
   361             for (var assoc : associations) {
   356             for (var assoc : associations) {
   362                 appendFileAssociation(xml, assoc);
   357                 appendFileAssociation(xml, assoc.data);
   363             }
   358             }
   364 
   359 
   365             xml.writeEndElement();
   360             xml.writeEndElement();
   366         });
   361         });
   367     }
   362     }
   368 
   363 
   369     private Map<String, Path> createFileAssociationIconFiles() throws
   364     private void addFileAssociationIconFiles(ShellCommands shellCommands)
   370             IOException {
   365             throws IOException {
   371         Map<String, Path> mimeTypeWithIconFile = new HashMap<>();
   366         Set<String> processedMimeTypes = new HashSet<>();
   372         for (var assoc : associations) {
   367         for (var assoc : associations) {
   373             File customFaIcon = FA_ICON.fetchFrom(assoc);
   368             if (assoc.iconSize <= 0) {
   374             if (customFaIcon == null || !customFaIcon.exists() || getSquareSizeOfImage(
   369                 // No icon.
   375                     customFaIcon) == 0) {
       
   376                 continue;
   370                 continue;
   377             }
   371             }
   378 
   372 
   379             String fname = iconFile.srcPath().getFileName().toString();
   373             for (var mimeType : assoc.data.mimeTypes) {
   380             if (fname.indexOf(".") > 0) {
   374                 if (processedMimeTypes.contains(mimeType)) {
   381                 fname = fname.substring(0, fname.lastIndexOf("."));
   375                     continue;
   382             }
   376                 }
   383 
   377 
   384             DesktopFile faIconFile = new DesktopFile(
   378                 processedMimeTypes.add(mimeType);
   385                     fname + "_fa_" + customFaIcon.getName());
   379 
   386 
   380                 // Create icon name for mime type from mime type.
   387             IOUtils.copyFile(customFaIcon, faIconFile.srcPath().toFile());
   381                 DesktopFile faIconFile = new DesktopFile(mimeType.replace(
   388 
   382                         File.separatorChar, '-') + IOUtils.getSuffix(
   389             mimeTypeWithIconFile.put(FA_CONTENT_TYPE.fetchFrom(assoc).get(0),
   383                                 assoc.data.iconPath));
   390                     faIconFile.installPath());
   384 
   391         }
   385                 IOUtils.copyFile(assoc.data.iconPath.toFile(),
   392         return mimeTypeWithIconFile;
   386                         faIconFile.srcPath().toFile());
       
   387 
       
   388                 shellCommands.addIcon(mimeType, faIconFile.installPath(),
       
   389                         assoc.iconSize);
       
   390             }
       
   391         }
   393     }
   392     }
   394 
   393 
   395     private void createDesktopFile(Map<String, String> data) throws IOException {
   394     private void createDesktopFile(Map<String, String> data) throws IOException {
   396         List<String> mimeTypes = getMimeTypeNamesFromFileAssociations();
   395         List<String> mimeTypes = getMimeTypeNamesFromFileAssociations();
   397         data.put("DESKTOP_MIMES", "MimeType=" + String.join(";", mimeTypes));
   396         data.put("DESKTOP_MIMES", "MimeType=" + String.join(";", mimeTypes));
   401                 .setSubstitutionData(data)
   400                 .setSubstitutionData(data)
   402                 .saveToFile(desktopFile.srcPath());
   401                 .saveToFile(desktopFile.srcPath());
   403     }
   402     }
   404 
   403 
   405     private List<String> getMimeTypeNamesFromFileAssociations() {
   404     private List<String> getMimeTypeNamesFromFileAssociations() {
   406         return associations.stream().map(
   405         return associations.stream()
   407                 a -> FA_CONTENT_TYPE.fetchFrom(a).get(0)).collect(
   406                 .map(fa -> fa.data.mimeTypes)
   408                         Collectors.toUnmodifiableList());
   407                 .flatMap(List::stream)
       
   408                 .collect(Collectors.toUnmodifiableList());
   409     }
   409     }
   410 
   410 
   411     private static int getSquareSizeOfImage(File f) {
   411     private static int getSquareSizeOfImage(File f) {
   412         try {
   412         try {
   413             BufferedImage bi = ImageIO.read(f);
   413             BufferedImage bi = ImageIO.read(f);
   414             if (bi.getWidth() == bi.getHeight()) {
   414             return Math.max(bi.getWidth(), bi.getHeight());
   415                 return bi.getWidth();
       
   416             }
       
   417         } catch (IOException e) {
   415         } catch (IOException e) {
   418             Log.verbose(e);
   416             Log.verbose(e);
   419         }
   417         }
   420         return 0;
   418         return 0;
       
   419     }
       
   420 
       
   421     private static int normalizeIconSize(int iconSize) {
       
   422         // If register icon with "uncommon" size, it will be ignored.
       
   423         // So find the best matching "common" size.
       
   424         List<Integer> commonIconSizes = List.of(16, 22, 32, 48, 64, 128);
       
   425 
       
   426         int idx = Collections.binarySearch(commonIconSizes, iconSize);
       
   427         if (idx < 0) {
       
   428             // Given icon size is greater than the largest common icon size.
       
   429             return commonIconSizes.get(commonIconSizes.size() - 1);
       
   430         }
       
   431 
       
   432         if (idx == 0) {
       
   433             // Given icon size is less or equal than the smallest common icon size.
       
   434             return commonIconSizes.get(idx);
       
   435         }
       
   436 
       
   437         int commonIconSize = commonIconSizes.get(idx);
       
   438         if (iconSize < commonIconSize) {
       
   439             // It is better to scale down original icon than to scale it up for
       
   440             // better visual quality.
       
   441             commonIconSize = commonIconSizes.get(idx - 1);
       
   442         }
       
   443 
       
   444         return commonIconSize;
   421     }
   445     }
   422 
   446 
   423     private static String stringifyShellCommands(String... commands) {
   447     private static String stringifyShellCommands(String... commands) {
   424         return stringifyShellCommands(Arrays.asList(commands));
   448         return stringifyShellCommands(Arrays.asList(commands));
   425     }
   449     }
   427     private static String stringifyShellCommands(List<String> commands) {
   451     private static String stringifyShellCommands(List<String> commands) {
   428         return String.join(System.lineSeparator(), commands.stream().filter(
   452         return String.join(System.lineSeparator(), commands.stream().filter(
   429                 s -> s != null && !s.isEmpty()).collect(Collectors.toList()));
   453                 s -> s != null && !s.isEmpty()).collect(Collectors.toList()));
   430     }
   454     }
   431 
   455 
       
   456     private static class LinuxFileAssociation {
       
   457         LinuxFileAssociation(FileAssociation fa) {
       
   458             this.data = fa;
       
   459             if (fa.iconPath != null && Files.isReadable(fa.iconPath)) {
       
   460                 iconSize = getSquareSizeOfImage(fa.iconPath.toFile());
       
   461             } else {
       
   462                 iconSize = -1;
       
   463             }
       
   464         }
       
   465 
       
   466         final FileAssociation data;
       
   467         final int iconSize;
       
   468     }
       
   469 
   432     private final PlatformPackage thePackage;
   470     private final PlatformPackage thePackage;
   433 
   471 
   434     private final List<Map<String, ? super Object>> associations;
   472     private final List<LinuxFileAssociation> associations;
   435 
   473 
   436     private final List<Map<String, ? super Object>> launchers;
   474     private final List<Map<String, ? super Object>> launchers;
   437 
   475 
   438     private final OverridableResource iconResource;
   476     private final OverridableResource iconResource;
   439     private final OverridableResource desktopFileResource;
   477     private final OverridableResource desktopFileResource;