2828 */
2929package org .scijava .desktop ;
3030
31+ import org .scijava .event .ContextCreatedEvent ;
32+ import org .scijava .event .EventHandler ;
3133import org .scijava .log .LogService ;
34+ import org .scijava .object .LazyObjects ;
35+ import org .scijava .object .ObjectService ;
3236import org .scijava .platform .PlatformService ;
3337import org .scijava .plugin .Parameter ;
3438import org .scijava .plugin .Plugin ;
39+ import org .scijava .prefs .PrefService ;
3540import org .scijava .service .AbstractService ;
3641import org .scijava .service .Service ;
42+ import org .scijava .thread .ThreadService ;
3743
3844import java .io .BufferedReader ;
3945import 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}
0 commit comments