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