Skip to content

Commit fe002f2

Browse files
committed
Defer file type registration appropriately
And auto-install desktop integrations on first run in a separate thread.
1 parent aeaa8dc commit fe002f2

3 files changed

Lines changed: 191 additions & 53 deletions

File tree

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

Lines changed: 114 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,18 @@
2828
*/
2929
package org.scijava.desktop;
3030

31+
import org.scijava.event.ContextCreatedEvent;
32+
import org.scijava.event.EventHandler;
3133
import org.scijava.log.LogService;
34+
import org.scijava.object.LazyObjects;
35+
import org.scijava.object.ObjectService;
3236
import org.scijava.platform.PlatformService;
3337
import org.scijava.plugin.Parameter;
3438
import org.scijava.plugin.Plugin;
39+
import org.scijava.prefs.PrefService;
3540
import org.scijava.service.AbstractService;
3641
import org.scijava.service.Service;
42+
import org.scijava.thread.ThreadService;
3743

3844
import java.io.BufferedReader;
3945
import java.io.IOException;
@@ -56,9 +62,22 @@ public class DefaultDesktopService extends AbstractService implements DesktopSer
5662
@Parameter
5763
private PlatformService platformService;
5864

65+
@Parameter
66+
private ObjectService objectService;
67+
68+
@Parameter
69+
private ThreadService threadService;
70+
71+
@Parameter(required = false)
72+
private PrefService prefs;
73+
5974
@Parameter(required = false)
6075
private LogService log;
6176

77+
/** Cached contents of {@code mime-types.txt}, keyed by extension (no leading dot). */
78+
private final Map<String, String> mimeDB = new HashMap<>();
79+
80+
/** Map of file extension to MIME type. */
6281
private final Map<String, String> fileTypes = new HashMap<>();
6382

6483
/**
@@ -67,8 +86,8 @@ public class DefaultDesktopService extends AbstractService implements DesktopSer
6786
*/
6887
private final Map<String, String> descriptions = new HashMap<>();
6988

70-
/** Cached contents of {@code mime-types.txt}, keyed by extension (no leading dot). */
71-
private Map<String, String> mimeDB;
89+
/** Whether lazy initialization is complete. */
90+
private boolean initialized;
7291

7392
@Override
7493
public void syncDesktopIntegration(final boolean webLinks,
@@ -123,76 +142,56 @@ public boolean isFileExtensionsEnabled() {
123142
}
124143

125144
@Override
126-
public void addFileType(final String ext,
145+
public void addFileType(final String extension,
127146
final String mimeType, final String description)
128147
{
129-
if (mimeDB == null) initMimeDB();
130-
131-
// Resolve the MIME type as needed and if possible.
132-
final String resolvedMimeType;
133-
if (mimeType == null || mimeType.isEmpty()) {
134-
// No MIME type was given -- try to resolve it from the file extension.
135-
// If not found, mark it with a wildcard sentinel for resolution
136-
// elsewhere, using a default MIME prefix of 'application'.
137-
resolvedMimeType = mimeDB.getOrDefault(ext, "application/*");
138-
}
139-
else if (mimeType.endsWith("/*")) {
140-
// A wildcard MIME type was given -- try to resolve it from the file extension.
141-
// If not found, leave the wildcard sentinel as is for resolution elsewhere.
142-
resolvedMimeType = mimeDB.getOrDefault(ext, mimeType);
143-
}
144-
else {
145-
// Assume an explicit MIME type was given -- use it verbatim.
146-
resolvedMimeType = mimeType;
147-
}
148-
149-
// Save the file extension -> MIME type association.
150-
fileTypes.put(ext, resolvedMimeType);
151-
if (log != null) {
152-
log.debug("Registered file extension '" + ext +
153-
"' as MIME type '" + resolvedMimeType + "'");
154-
}
148+
objectService.addObject(new FileType(extension, mimeType, description));
149+
}
155150

156-
// Save the file extension -> description association.
157-
if (description == null) return; // No description to register.
158-
if (descriptions.containsKey(ext)) {
159-
if (log != null) {
160-
log.debug("Ignoring description '" + description +
161-
"' for file extension '" + ext +
162-
"' with existing description '" + descriptions.get(ext) + "'");
163-
}
164-
}
165-
else {
166-
descriptions.put(ext, description);
167-
if (log != null) {
168-
log.debug("Registered description '" + description +
169-
"' for file extension '" + ext + "'");
170-
}
171-
}
151+
@Override
152+
public void addFileTypes(LazyObjects<FileType> fileTypes) {
153+
objectService.getIndex().addLater(fileTypes);
172154
}
173155

174156
@Override
175157
public Map<String, String> getFileTypes() {
158+
if (fileTypes == null) initFileTypes();
176159
return Collections.unmodifiableMap(fileTypes);
177160
}
178161

179162
@Override
180163
public String getDescription(final String extension) {
164+
if (fileTypes == null) initFileTypes();
181165
return descriptions.get(extension);
182166
}
183167

168+
// -- Event handlers --
169+
170+
@EventHandler
171+
public void onEvent(ContextCreatedEvent event) {
172+
maybeAutoInstallDesktopIntegrations();
173+
}
174+
184175
// -- Helper methods - lazy initialization --
185176

186-
/** Initializes {@link #mimeDB} from the built-in {@code mime-types.txt} resource. */
187-
private synchronized void initMimeDB() {
188-
if (mimeDB != null) return; // already initialized
177+
private synchronized void initFileTypes() {
178+
if (initialized) return;
179+
initMimeDB();
180+
for (var fileType : objectService.getObjects(FileType.class)) {
181+
resolveFileType(fileType);
182+
}
183+
initialized = true;
184+
}
189185

186+
/** Initializes {@link #mimeDB} from the built-in {@code mime-types.txt} resource. */
187+
private void initMimeDB() {
190188
final Map<String, String> db = new HashMap<>();
191189
final String resource = "mime-types.txt";
192-
try (final InputStream is = getClass().getResourceAsStream(resource);
193-
final BufferedReader reader = new BufferedReader(
194-
new InputStreamReader(is, StandardCharsets.UTF_8)))
195-
{
190+
try (
191+
final InputStream is = getClass().getResourceAsStream(resource);
192+
final BufferedReader reader = new BufferedReader(
193+
new InputStreamReader(is, StandardCharsets.UTF_8))
194+
) {
196195
String line;
197196
while ((line = reader.readLine()) != null) {
198197
if (line.startsWith("#") || line.isBlank()) continue;
@@ -223,7 +222,54 @@ private synchronized void initMimeDB() {
223222
catch (final IOException e) {
224223
if (log != null) log.error("Failed to load MIME types database", e);
225224
}
226-
mimeDB = db;
225+
}
226+
227+
private void resolveFileType(FileType fileType) {
228+
String extension = fileType.extension;
229+
String mimeType = fileType.mimeType;
230+
String description = fileType.description;
231+
232+
// Resolve the MIME type as needed and if possible.
233+
final String resolvedMimeType;
234+
if (mimeType == null || mimeType.isEmpty()) {
235+
// No MIME type was given -- try to resolve it from the file extension.
236+
// If not found, mark it with a wildcard sentinel for resolution
237+
// elsewhere, using a default MIME prefix of 'application'.
238+
resolvedMimeType = mimeDB.getOrDefault(extension, "application/*");
239+
}
240+
else if (mimeType.endsWith("/*")) {
241+
// A wildcard MIME type was given -- try to resolve it from the file extension.
242+
// If not found, leave the wildcard sentinel as is for resolution elsewhere.
243+
resolvedMimeType = mimeDB.getOrDefault(extension, mimeType);
244+
}
245+
else {
246+
// Assume an explicit MIME type was given -- use it verbatim.
247+
resolvedMimeType = mimeType;
248+
}
249+
250+
// Save the file extension -> MIME type association.
251+
fileTypes.put(extension, resolvedMimeType);
252+
if (log != null) {
253+
log.debug("Registered file extension '" + extension +
254+
"' as MIME type '" + resolvedMimeType + "'");
255+
}
256+
257+
// Save the file extension -> description association.
258+
if (description == null) return; // No description to register.
259+
if (descriptions.containsKey(extension)) {
260+
if (log != null) {
261+
log.debug("Ignoring description '" + description +
262+
"' for file extension '" + extension +
263+
"' with existing description '" + descriptions.get(extension) + "'");
264+
}
265+
}
266+
else {
267+
descriptions.put(extension, description);
268+
if (log != null) {
269+
log.debug("Registered description '" + description +
270+
"' for file extension '" + extension + "'");
271+
}
272+
}
227273
}
228274

229275
// -- Helper methods --
@@ -233,4 +279,19 @@ private Stream<DesktopIntegrationProvider> desktopPlatforms() {
233279
.filter(p -> p instanceof DesktopIntegrationProvider) //
234280
.map(p -> (DesktopIntegrationProvider) p);
235281
}
282+
283+
private void maybeAutoInstallDesktopIntegrations() {
284+
// Auto-install desktop integrations on first run.
285+
286+
// But if we've done it before, don't do it again.
287+
final boolean installedOnce = prefs != null &&
288+
prefs.getBoolean(DesktopService.class, "installedOnce", false);
289+
if (installedOnce) return;
290+
291+
// We haven't installed the integration before now! So here we go.
292+
// We use a dedicated thread to avoid blocking context creation completion;
293+
// nothing in the desktop registration needs to be completed synchronously;
294+
// we just want to complete the work as soon as reasonably possible.
295+
threadService.run(() -> syncDesktopIntegration(true, true, true));
296+
}
236297
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
*/
2929
package org.scijava.desktop;
3030

31+
import org.scijava.object.LazyObjects;
3132
import org.scijava.service.SciJavaService;
3233

3334
import java.util.List;
@@ -137,6 +138,13 @@ default void addFileTypes(final List<String> extensions,
137138
}
138139
}
139140

141+
/**
142+
* Adds a batch of file types to be coelesced lazily at registration time.
143+
*
144+
* @param fileTypes Lazy callback to be invoked later when file types are needed.
145+
*/
146+
void addFileTypes(final LazyObjects<FileType> fileTypes);
147+
140148
/**
141149
* Gets the map of supported file types (extension → MIME type).
142150
* <p>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* #%L
3+
* Desktop integration for SciJava.
4+
* %%
5+
* Copyright (C) 2010 - 2026 SciJava developers.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
30+
package org.scijava.desktop;
31+
32+
/**
33+
* Struct storing metadata about a particular file type.
34+
*
35+
* @author Curtis Rueden
36+
*/
37+
public class FileType {
38+
39+
/** File extension without leading dot (e.g. {@code "png"}). */
40+
public final String extension;
41+
42+
/**
43+
* MIME type (e.g. {@code "image/png"}), or a wildcard of
44+
* the form {@code "category/*"} (e.g. {@code "image/*"}) if
45+
* the specific type is unknown. Wildcard values are resolved
46+
* against the bundled MIME database by extension; if still
47+
* unresolved, the sentinel is preserved for platform-specific
48+
* code to handle at OS registration time.
49+
*/
50+
public final String mimeType;
51+
52+
/**
53+
* Human-readable description of the file type
54+
* (e.g. {@code "Gatan Digital Micrograph image"}), used
55+
* as the label when registering a custom MIME type, or
56+
* {@code null} to synthesize one from the extension.
57+
*/
58+
public final String description;
59+
60+
public FileType(String extension, String mimeType) {
61+
this(extension, mimeType, null);
62+
}
63+
64+
public FileType(String extension, String mimeType, String description) {
65+
this.extension = extension;
66+
this.mimeType = mimeType;
67+
this.description = description;
68+
}
69+
}

0 commit comments

Comments
 (0)