Skip to content

Commit a3db9e5

Browse files
ctruedenclaude
andcommitted
Detect which mime types are known vs. custom
This reworks the DesktopService API and also switches the mime-types.txt format to allow description. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 364a282 commit a3db9e5

File tree

4 files changed

+1611
-1468
lines changed

4 files changed

+1611
-1468
lines changed

src/main/java/org/scijava/desktop/DefaultDesktopService.java

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -56,34 +56,74 @@ public class DefaultDesktopService extends AbstractService implements DesktopSer
5656

5757
private final Map<String, String> fileTypes = new HashMap<>();
5858

59+
/**
60+
* Description of each file type extension, as registered by
61+
* {@link #addFileType} and {@link #addFileTypes}.
62+
*/
63+
private final Map<String, String> descriptions = new HashMap<>();
64+
5965
/** Cached contents of {@code mime-types.txt}, keyed by extension (no leading dot). */
6066
private Map<String, String> mimeDB;
6167

6268
@Override
63-
public void addFileType(String ext, String mimeType) {
64-
fileTypes.put(ext, mimeType);
65-
}
66-
67-
@Override
68-
public void addFileTypes(String mimePrefix, String... extensions) {
69+
public void addFileType(final String ext,
70+
final String mimeType, final String description)
71+
{
6972
if (mimeDB == null) initMimeDB();
70-
for (final String ext : extensions) {
71-
if (ext == null || ext.isEmpty()) continue;
72-
final String mimeType = mimeDB.getOrDefault(ext, mimePrefix + "/x-" + ext);
73-
addFileType(ext, mimeType);
73+
74+
// Resolve the MIME type as needed and if possible.
75+
final String resolvedMimeType;
76+
if (mimeType == null || mimeType.isEmpty()) {
77+
// No MIME type was given -- try to resolve it from the file extension.
78+
// If not found, mark it with a wildcard sentinel for resolution
79+
// elsewhere, using a default MIME prefix of 'application'.
80+
resolvedMimeType = mimeDB.getOrDefault(ext, "application/*");
81+
}
82+
else if (mimeType.endsWith("/*")) {
83+
// A wildcard MIME type was given -- try to resolve it from the file extension.
84+
// If not found, leave the wildcard sentinel as is for resolution elsewhere.
85+
resolvedMimeType = mimeDB.getOrDefault(ext, mimeType);
86+
}
87+
else {
88+
// Assume an explicit MIME type was given -- use it verbatim.
89+
resolvedMimeType = mimeType;
7490
}
75-
}
7691

77-
@Override
78-
public void addFileTypes(Map<String, String> extToMimeType) {
79-
fileTypes.putAll(extToMimeType);
92+
// Save the file extension -> MIME type association.
93+
fileTypes.put(ext, resolvedMimeType);
94+
if (log != null) {
95+
log.debug("Registered file extension '" + ext +
96+
"' as MIME type '" + resolvedMimeType + "'");
97+
}
98+
99+
// Save the file extension -> description association.
100+
if (description == null) return; // No description to register.
101+
if (descriptions.containsKey(ext)) {
102+
if (log != null) {
103+
log.debug("Ignoring description '" + description +
104+
"' for file extension '" + ext +
105+
"' with existing description '" + descriptions.get(ext) + "'");
106+
}
107+
}
108+
else {
109+
descriptions.put(ext, description);
110+
if (log != null) {
111+
log.debug("Registered description '" + description +
112+
"' for file extension '" + ext + "'");
113+
}
114+
}
80115
}
81116

82117
@Override
83118
public Map<String, String> getFileTypes() {
84119
return Collections.unmodifiableMap(fileTypes);
85120
}
86121

122+
@Override
123+
public String getDescription(final String extension) {
124+
return descriptions.get(extension);
125+
}
126+
87127
// -- Helper methods - lazy initialization --
88128

89129
/** Initializes {@link #mimeDB} from the built-in {@code mime-types.txt} resource. */
@@ -99,14 +139,28 @@ private synchronized void initMimeDB() {
99139
String line;
100140
while ((line = reader.readLine()) != null) {
101141
if (line.startsWith("#") || line.isBlank()) continue;
102-
final int tab = line.indexOf('\t');
103-
if (tab < 0) {
142+
final int pipe1 = line.indexOf('|');
143+
if (pipe1 < 0) {
104144
if (log != null) log.warn("Invalid MIME types DB line: " + line);
105145
continue;
106146
}
107-
final String ext = line.substring(0, tab).trim();
108-
final String mime = line.substring(tab + 1).trim();
147+
final String ext = line.substring(0, pipe1).trim();
148+
final String rest = line.substring(pipe1 + 1).trim();
149+
final int pipe2 = rest.indexOf('|');
150+
final String mime;
151+
final String description;
152+
if (pipe2 < 0) {
153+
mime = rest;
154+
description = null;
155+
}
156+
else {
157+
mime = rest.substring(0, pipe2).trim();
158+
description = rest.substring(pipe2 + 1).trim();
159+
}
109160
db.putIfAbsent(ext, mime);
161+
if (description != null && !description.isEmpty()) {
162+
descriptions.put(ext, description);
163+
}
110164
}
111165
}
112166
catch (final IOException e) {

src/main/java/org/scijava/desktop/DesktopService.java

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
*/
2929
package org.scijava.desktop;
3030

31-
import org.scijava.desktop.links.LinkHandler;
3231
import org.scijava.service.SciJavaService;
3332

33+
import java.util.List;
3434
import java.util.Map;
3535

3636
/**
@@ -41,39 +41,83 @@
4141
public interface DesktopService extends SciJavaService {
4242

4343
/**
44-
* Adds to the set of supported file types, looking up each extension's MIME
45-
* type from the built-in database. If an extension is not found in the
46-
* database, it falls back to {@code mimePrefix/x-ext} (e.g. passing prefix
47-
* {@code "image"} for unknown extension {@code "foo"} yields
48-
* {@code "image/x-foo"}).
44+
* Adds a single file type.
4945
*
50-
* @param mimePrefix The MIME type prefix (e.g. {@code "image"},
51-
* {@code "application"}) used when an extension is absent
52-
* from the database.
53-
* @param extensions One or more file extensions to register (without leading
54-
* dot, e.g. {@code "tiff"}, {@code "png"}).
46+
* @param ext File extension without leading dot (e.g. {@code "png"}).
47+
* @param mimeType MIME type (e.g. {@code "image/png"}), or a wildcard of
48+
* the form {@code "category/*"} (e.g. {@code "image/*"}) if
49+
* the specific type is unknown. Wildcard values are resolved
50+
* against the bundled MIME database by extension; if still
51+
* unresolved, the sentinel is preserved for platform-specific
52+
* code to handle at OS registration time.
5553
*/
56-
void addFileTypes(String mimePrefix, String... extensions);
54+
default void addFileType(String ext, String mimeType) {
55+
addFileType(ext, mimeType, null);
56+
}
5757

5858
/**
59-
* Adds to the set of supported file types using an explicit mapping.
59+
* Adds a single file type.
6060
*
61-
* @param extToMimeType Map from file extension (without leading dot,
62-
* e.g. {@code "png"}) to MIME type
63-
* (e.g. {@code "image/png"}).
61+
* @param ext File extension without leading dot (e.g. {@code "png"}).
62+
* @param mimeType MIME type (e.g. {@code "image/png"}), or a wildcard of
63+
* the form {@code "category/*"} (e.g. {@code "image/*"}) if
64+
* the specific type is unknown. Wildcard values are resolved
65+
* against the bundled MIME database by extension; if still
66+
* unresolved, the sentinel is preserved for platform-specific
67+
* code to handle at OS registration time.
68+
* @param description Human-readable description of the file type
69+
* (e.g. {@code "Gatan Digital Micrograph image"}), used
70+
* as the label when registering a custom MIME type, or
71+
* {@code null} to synthesize one from the extension.
6472
*/
65-
void addFileTypes(Map<String, String> extToMimeType);
73+
void addFileType(String ext, String mimeType, String description);
6674

6775
/**
68-
* Adds a single file type with an explicit MIME type.
76+
* Adds a batch of file types sharing the same MIME type or wildcard.
77+
* Equivalent to calling {@link #addFileType(String, String)} for each
78+
* extension.
6979
*
70-
* @param ext File extension without leading dot (e.g. {@code "png"}).
71-
* @param mimeType MIME type (e.g. {@code "image/png"}).
80+
* @param extensions File extensions without leading dot (e.g. {@code "tiff"},
81+
* {@code "tif"}).
82+
* @param mimeType MIME type or wildcard; see {@link #addFileType(String, String)}.
83+
*/
84+
default void addFileTypes(List<String> extensions, String mimeType) {
85+
addFileTypes(extensions, mimeType, null);
86+
}
87+
88+
/**
89+
* Adds a batch of file types sharing the same MIME type or wildcard.
90+
* Equivalent to calling {@link #addFileType(String, String, String)} for
91+
* each extension.
92+
*
93+
* @param extensions File extensions without leading dot (e.g. {@code "tiff"},
94+
* {@code "tif"}).
95+
* @param mimeType MIME type or wildcard; see {@link #addFileType(String, String, String)}.
96+
* @param description Shared description for all extensions in this batch;
97+
* see {@link #addFileType(String, String, String)}.
7298
*/
73-
void addFileType(String ext, String mimeType);
99+
default void addFileTypes(final List<String> extensions,
100+
final String mimeType, final String description)
101+
{
102+
for (final String ext : extensions) {
103+
addFileType(ext, mimeType, description);
104+
}
105+
}
74106

75107
/**
76-
* Gets the map of supported file types.
108+
* Gets the map of supported file types (extension → -> MIME type).
109+
* <p>
110+
* Values ending in {@code "/*"} (e.g. {@code "image/*"}) are unresolved
111+
* sentinels, meaning the specific MIME type is not yet known. Callers that
112+
* write OS registrations should resolve these against the system MIME
113+
* database and synthesize a concrete type (e.g. {@code "image/x-dm3"}) if
114+
* still unresolved.
115+
* </p>
77116
*/
78117
Map<String, String> getFileTypes();
118+
119+
/**
120+
* Gets the description for a given file type extension, or null if none.
121+
*/
122+
String getDescription(String extension);
79123
}

src/main/java/org/scijava/desktop/platform/linux/LinuxPlatform.java

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import java.nio.file.Paths;
5050
import java.nio.file.StandardOpenOption;
5151
import java.util.Collections;
52+
import java.util.HashSet;
5253
import java.util.LinkedHashMap;
5354
import java.util.Map;
5455
import java.util.Set;
@@ -82,6 +83,9 @@ public class LinuxPlatform extends AbstractPlatform
8283
@Parameter(required = false)
8384
private LogService log;
8485

86+
/** Cached set of MIME types known to the system's shared-mime-info database. */
87+
private Set<String> systemMimeTypes;
88+
8589
// -- Platform methods --
8690

8791
@Override
@@ -217,7 +221,8 @@ public boolean isFileExtensionsEnabled() {
217221
if (df.hasMimeType(mimeType)) return true;
218222
}
219223
return false;
220-
} catch (final IOException e) {
224+
}
225+
catch (final IOException e) {
221226
if (log != null) {
222227
log.debug("Failed to check file extensions status", e);
223228
}
@@ -254,7 +259,8 @@ public void setFileExtensionsEnabled(final boolean enable) throws IOException {
254259
if (log != null) {
255260
log.info("Registered " + mimeMapping.size() + " file extension MIME types");
256261
}
257-
} else {
262+
}
263+
else {
258264
// Remove file extension MIME types from .desktop file.
259265
// Keep URI scheme handlers (x-scheme-handler/...).
260266
final DesktopFile df = getOrCreateDesktopFile();
@@ -408,20 +414,57 @@ private Map<String, String> fileTypes() {
408414
Collections.emptyMap() : desktopService.getFileTypes();
409415
}
410416

417+
// -- Helper methods - lazy initialization --
418+
419+
/**
420+
* Initializes {@link #systemMimeTypes} by walking {@code /usr/share/mime/}.
421+
* <p>
422+
* The compiled shared-mime-info database is organized as
423+
* {@code /usr/share/mime/type/subtype.xml}, so each XML file path below the
424+
* root (excluding the {@code packages/} source tree) directly encodes a MIME
425+
* type. If the directory is absent or unreadable, the set is left empty and
426+
* all app-registered types will be treated as custom.
427+
* </p>
428+
*/
429+
private synchronized void initSystemMimeTypes() {
430+
if (systemMimeTypes != null) return; // already initialized
431+
432+
final Set<String> types = new HashSet<>();
433+
final Path mimeRoot = Paths.get("/usr/share/mime");
434+
if (Files.isDirectory(mimeRoot)) {
435+
try (final var stream = Files.walk(mimeRoot)) {
436+
stream
437+
.filter(p -> p.toString().endsWith(".xml"))
438+
.filter(p -> !p.toString().contains("/packages/"))
439+
.forEach(p -> {
440+
final String rel = mimeRoot.relativize(p).toString();
441+
// rel is e.g. "image/png.xml" -> strip ".xml" -> "image/png"
442+
types.add(rel.substring(0, rel.length() - 4));
443+
});
444+
}
445+
catch (final IOException | RuntimeException e) {
446+
if (log != null) log.warn("Could not read system MIME database", e);
447+
}
448+
}
449+
if (log != null) log.debug("Loaded " + types.size() + " system MIME types");
450+
systemMimeTypes = types;
451+
}
452+
411453
/**
412-
* Registers custom MIME types for formats that don't have standard types.
454+
* Registers custom MIME types for formats not already known to the system.
413455
* Creates {@code ~/.local/share/mime/packages/[appName].xml} and runs
414456
* {@code update-mime-database}.
415457
*/
416458
private void registerCustomMimeTypes(final Map<String, String> mimeMapping)
417459
throws IOException
418460
{
419-
// Separate standard from custom MIME types.
461+
if (systemMimeTypes == null) initSystemMimeTypes();
462+
463+
// Collect only types absent from the system MIME database.
420464
final Map<String, String> customTypes = new LinkedHashMap<>();
421465
for (final Map.Entry<String, String> entry : mimeMapping.entrySet()) {
422466
final String mimeType = entry.getValue();
423-
// Custom types use application/x- prefix.
424-
if (mimeType.startsWith("application/x-")) {
467+
if (!systemMimeTypes.contains(mimeType)) {
425468
customTypes.put(entry.getKey(), mimeType);
426469
}
427470
}
@@ -457,10 +500,12 @@ private void registerCustomMimeTypes(final Map<String, String> mimeMapping)
457500
if (log != null) {
458501
log.warn("update-mime-database exited with code " + exitCode);
459502
}
460-
} else if (log != null) {
503+
}
504+
else if (log != null) {
461505
log.info("Registered " + customTypes.size() + " custom MIME types");
462506
}
463-
} catch (final Exception e) {
507+
}
508+
catch (final Exception e) {
464509
if (log != null) {
465510
log.error("Failed to run update-mime-database", e);
466511
}

0 commit comments

Comments
 (0)