001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import java.awt.GraphicsEnvironment;
005import java.awt.Toolkit;
006import java.awt.event.KeyEvent;
007import java.io.BufferedReader;
008import java.io.File;
009import java.io.IOException;
010import java.io.InputStreamReader;
011import java.nio.charset.StandardCharsets;
012import java.security.KeyStore;
013import java.security.KeyStoreException;
014import java.security.NoSuchAlgorithmException;
015import java.security.cert.CertificateException;
016import java.security.cert.X509Certificate;
017import java.text.DateFormat;
018import java.util.Collection;
019import java.util.Collections;
020import java.util.Date;
021import java.util.List;
022
023import org.openstreetmap.josm.data.projection.datum.NTV2Proj4DirGridShiftFileSource;
024import org.openstreetmap.josm.io.CertificateAmendment.NativeCertAmend;
025import org.openstreetmap.josm.spi.preferences.Config;
026import org.openstreetmap.josm.tools.date.DateUtils;
027
028/**
029 * This interface allows platform (operating system) dependent code
030 * to be bundled into self-contained classes.
031 * @since 1023
032 */
033public interface PlatformHook {
034
035    /**
036     * Visitor to construct a PlatformHook from a given {@link Platform} object.
037     */
038    PlatformVisitor<PlatformHook> CONSTRUCT_FROM_PLATFORM = new PlatformVisitor<PlatformHook>() {
039        @Override
040        public PlatformHook visitUnixoid() {
041            return new PlatformHookUnixoid();
042        }
043
044        @Override
045        public PlatformHook visitWindows() {
046            return new PlatformHookWindows();
047        }
048
049        @Override
050        public PlatformHook visitOsx() {
051            return new PlatformHookOsx();
052        }
053    };
054
055    /**
056     * Get the platform corresponding to this platform hook.
057     * @return the platform corresponding to this platform hook
058     */
059    Platform getPlatform();
060
061    /**
062      * The preStartupHook will be called extremely early. It is
063      * guaranteed to be called before the GUI setup has started.
064      *
065      * Reason: On OSX we need to inform the Swing libraries
066      * that we want to be integrated with the OS before we setup our GUI.
067      */
068    default void preStartupHook() {
069        // Do nothing
070    }
071
072    /**
073      * The afterPrefStartupHook will be called early, but after
074      * the preferences have been loaded and basic processing of
075      * command line arguments is finished.
076      * It is guaranteed to be called before the GUI setup has started.
077      */
078    default void afterPrefStartupHook() {
079        // Do nothing
080    }
081
082    /**
083      * The startupHook will be called early, but after the GUI
084      * setup has started.
085      *
086      * Reason: On OSX we need to register some callbacks with the
087      * OS, so we'll receive events from the system menu.
088     * @param callback Java expiration callback, providing GUI feedback
089     * @since 12270 (signature)
090      */
091    default void startupHook(JavaExpirationCallback callback) {
092        // Do nothing
093    }
094
095    /**
096      * The openURL hook will be used to open an URL in the
097      * default web browser.
098     * @param url The URL to open
099     * @throws IOException if any I/O error occurs
100      */
101    void openUrl(String url) throws IOException;
102
103    /**
104      * The initSystemShortcuts hook will be called by the
105      * Shortcut class after the modifier groups have been read
106      * from the config, but before any shortcuts are read from
107      * it or registered from within the application.
108      *
109      * Please note that you are not allowed to register any
110      * shortuts from this hook, but only "systemCuts"!
111      *
112      * BTW: SystemCuts should be named "system:&lt;whatever&gt;",
113      * and it'd be best if sou'd recycle the names already used
114      * by the Windows and OSX hooks. Especially the later has
115      * really many of them.
116      *
117      * You should also register any and all shortcuts that the
118      * operation system handles itself to block JOSM from trying
119      * to use them---as that would just not work. Call setAutomatic
120      * on them to prevent the keyboard preferences from allowing the
121      * user to change them.
122      */
123    void initSystemShortcuts();
124
125    /**
126      * The makeTooltip hook will be called whenever a tooltip for
127      * a menu or button is created.
128      *
129      * Tooltips are usually not system dependent, unless the
130      * JVM is too dumb to provide correct names for all the keys.
131      *
132      * Some LAFs don't understand HTML, such as the OSX LAFs.
133      *
134     * @param name Tooltip text to display
135     * @param sc Shortcut associated (to display accelerator between parenthesis)
136     * @return Full tooltip text (name + accelerator)
137     * @since 1084
138     * @deprecated Use {@link Shortcut#makeTooltip} instead.
139      */
140    @Deprecated
141    default String makeTooltip(String name, Shortcut sc) {
142        return Shortcut.makeTooltip(name, sc != null ? sc.getKeyStroke() : null);
143    }
144
145    /**
146     * Returns the default LAF to be used on this platform to look almost as a native application.
147     * @return The default native LAF for this platform
148     */
149    String getDefaultStyle();
150
151    /**
152     * Determines if the platform allows full-screen.
153     * @return {@code true} if full screen is allowed, {@code false} otherwise
154     */
155    default boolean canFullscreen() {
156        return !GraphicsEnvironment.isHeadless() &&
157                GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().isFullScreenSupported();
158    }
159
160    /**
161     * Renames a file.
162     * @param from Source file
163     * @param to Target file
164     * @return {@code true} if the file has been renamed, {@code false} otherwise
165     */
166    default boolean rename(File from, File to) {
167        return from.renameTo(to);
168    }
169
170    /**
171     * Returns a detailed OS description (at least family + version).
172     * @return A detailed OS description.
173     * @since 5850
174     */
175    String getOSDescription();
176
177    /**
178     * Returns OS build number.
179     * @return OS build number.
180     * @since 12217
181     */
182    default String getOSBuildNumber() {
183        return "";
184    }
185
186    /**
187     * Setup system keystore to add JOSM HTTPS certificate (for remote control).
188     * @param entryAlias The entry alias to use
189     * @param trustedCert the JOSM certificate for localhost
190     * @return {@code true} if something has changed as a result of the call (certificate installation, etc.)
191     * @throws KeyStoreException in case of error
192     * @throws IOException in case of error
193     * @throws CertificateException in case of error
194     * @throws NoSuchAlgorithmException in case of error
195     * @since 7343
196     */
197    default boolean setupHttpsCertificate(String entryAlias, KeyStore.TrustedCertificateEntry trustedCert)
198            throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
199        // TODO setup HTTPS certificate on Unix and OS X systems
200        return false;
201    }
202
203    /**
204     * Returns the {@code X509Certificate} matching the given certificate amendment information.
205     * @param certAmend certificate amendment
206     * @return the {@code X509Certificate} matching the given certificate amendment information, or {@code null}
207     * @throws KeyStoreException in case of error
208     * @throws IOException in case of error
209     * @throws CertificateException in case of error
210     * @throws NoSuchAlgorithmException in case of error
211     * @since 13450
212     */
213    default X509Certificate getX509Certificate(NativeCertAmend certAmend)
214            throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
215        return null;
216    }
217
218    /**
219     * Executes a native command and returns the first line of standard output.
220     * @param command array containing the command to call and its arguments.
221     * @return first stripped line of standard output
222     * @throws IOException if an I/O error occurs
223     * @since 12217
224     */
225    default String exec(String... command) throws IOException {
226        Process p = Runtime.getRuntime().exec(command);
227        try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
228            return Utils.strip(input.readLine());
229        }
230    }
231
232    /**
233     * Returns the platform-dependent default cache directory.
234     * @return the platform-dependent default cache directory
235     * @since 7829
236     */
237    File getDefaultCacheDirectory();
238
239    /**
240     * Returns the platform-dependent default preferences directory.
241     * @return the platform-dependent default preferences directory
242     * @since 7831
243     */
244    File getDefaultPrefDirectory();
245
246    /**
247     * Returns the platform-dependent default user data directory.
248     * @return the platform-dependent default user data directory
249     * @since 7834
250     */
251    File getDefaultUserDataDirectory();
252
253    /**
254     * Returns the list of platform-dependent default datum shifting directories for the PROJ.4 library.
255     * @return the list of platform-dependent default datum shifting directories for the PROJ.4 library
256     * @since 11642
257     */
258    default List<File> getDefaultProj4NadshiftDirectories() {
259        return getPlatform().accept(NTV2Proj4DirGridShiftFileSource.getInstance());
260    }
261
262    /**
263     * Determines if the JVM is OpenJDK-based.
264     * @return {@code true} if {@code java.home} contains "openjdk", {@code false} otherwise
265     * @since 12219
266     */
267    default boolean isOpenJDK() {
268        String javaHome = Utils.getSystemProperty("java.home");
269        return javaHome != null && javaHome.contains("openjdk");
270    }
271
272    /**
273     * Returns extended modifier key used as the appropriate accelerator key for menu shortcuts.
274     * It is advised everywhere to use {@link Toolkit#getMenuShortcutKeyMask()} to get the cross-platform modifier, but:
275     * <ul>
276     * <li>it returns KeyEvent.CTRL_MASK instead of KeyEvent.CTRL_DOWN_MASK. We used the extended
277     *    modifier for years, and Oracle recommends to use it instead, so it's best to keep it</li>
278     * <li>the method throws a HeadlessException ! So we would need to handle it for unit tests anyway</li>
279     * </ul>
280     * @return extended modifier key used as the appropriate accelerator key for menu shortcuts
281     * @since 12748 (as a replacement to {@code GuiHelper.getMenuShortcutKeyMaskEx()})
282     */
283    default int getMenuShortcutKeyMaskEx() {
284        // To remove when switching to Java 10+, and use Toolkit.getMenuShortcutKeyMaskEx instead
285        return KeyEvent.CTRL_DOWN_MASK;
286    }
287
288    /**
289     * Called when an outdated version of Java is detected at startup.
290     * @since 12270
291     */
292    @FunctionalInterface
293    interface JavaExpirationCallback {
294        /**
295         * Asks user to update its version of Java.
296         * @param updVersion target update version
297         * @param url download URL
298         * @param major true for a migration towards a major version of Java (8:9), false otherwise
299         * @param eolDate the EOL/expiration date
300         */
301        void askUpdateJava(String updVersion, String url, String eolDate, boolean major);
302    }
303
304    /**
305     * Checks if the running version of Java has expired, proposes to user to update it if needed.
306     * @param callback Java expiration callback
307     * @since 12270 (signature)
308     * @since 12219
309     */
310    default void checkExpiredJava(JavaExpirationCallback callback) {
311        Date expiration = Utils.getJavaExpirationDate();
312        if (expiration != null && expiration.before(new Date())) {
313            String version = Utils.getJavaLatestVersion();
314            callback.askUpdateJava(version != null ? version : "latest",
315                    Config.getPref().get("java.update.url", "https://www.java.com/download"),
316                    DateUtils.getDateFormat(DateFormat.MEDIUM).format(expiration), false);
317        }
318    }
319
320    /**
321     * Called when interfacing with native OS functions. Currently only used with macOS.
322     * The callback must perform all GUI-related tasks associated to an OS request.
323     * The non-GUI, platform-specific tasks, are usually performed by the {@code PlatformHook}.
324     * @since 12695
325     */
326    interface NativeOsCallback {
327        /**
328         * macOS: Called when JOSM is asked to open a list of files.
329         * @param files list of files to open
330         */
331        void openFiles(List<File> files);
332
333        /**
334         * macOS: Invoked when JOSM is asked to quit.
335         * @return {@code true} if JOSM has been closed, {@code false} if the user has cancelled the operation.
336         */
337        boolean handleQuitRequest();
338
339        /**
340         * macOS: Called when JOSM is asked to show it's about dialog.
341         */
342        void handleAbout();
343
344        /**
345         * macOS: Called when JOSM is asked to show it's preferences UI.
346         */
347        void handlePreferences();
348    }
349
350    /**
351     * Registers the native OS callback. Currently only needed for macOS.
352     * @param callback the native OS callback
353     * @since 12695
354     */
355    default void setNativeOsCallback(NativeOsCallback callback) {
356        // To be implemented if needed
357    }
358
359    /**
360     * Resolves a file link to its destination file.
361     * @param file file (link or regular file)
362     * @return destination file in case of a file link, file if regular
363     * @since 13691
364     */
365    default File resolveFileLink(File file) {
366        // Override if needed
367        return file;
368    }
369
370    /**
371     * Returns a set of possible platform specific directories where resources could be stored.
372     * @return A set of possible platform specific directories where resources could be stored.
373     * @since 14144
374     */
375    default Collection<String> getPossiblePreferenceDirs() {
376        return Collections.emptyList();
377    }
378}