001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Desktop;
007import java.awt.Dimension;
008import java.awt.GraphicsEnvironment;
009import java.awt.event.KeyEvent;
010import java.io.BufferedReader;
011import java.io.BufferedWriter;
012import java.io.File;
013import java.io.FileInputStream;
014import java.io.IOException;
015import java.io.InputStreamReader;
016import java.io.OutputStream;
017import java.io.OutputStreamWriter;
018import java.io.Writer;
019import java.net.URI;
020import java.net.URISyntaxException;
021import java.nio.charset.StandardCharsets;
022import java.nio.file.FileSystems;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.nio.file.Paths;
026import java.security.KeyStore;
027import java.security.KeyStoreException;
028import java.security.NoSuchAlgorithmException;
029import java.security.cert.CertificateException;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Collection;
033import java.util.List;
034import java.util.Properties;
035
036import javax.swing.JOptionPane;
037
038import org.openstreetmap.josm.Main;
039import org.openstreetmap.josm.data.Preferences.pref;
040import org.openstreetmap.josm.data.Preferences.writeExplicitly;
041import org.openstreetmap.josm.gui.ExtendedDialog;
042import org.openstreetmap.josm.gui.util.GuiHelper;
043
044/**
045 * {@code PlatformHook} base implementation.
046 *
047 * Don't write (Main.platform instanceof PlatformHookUnixoid) because other platform
048 * hooks are subclasses of this class.
049 */
050public class PlatformHookUnixoid implements PlatformHook {
051
052    /**
053     * Simple data class to hold information about a font.
054     *
055     * Used for fontconfig.properties files.
056     */
057    public static class FontEntry {
058        /**
059         * The character subset. Basically a free identifier, but should be unique.
060         */
061        @pref
062        public String charset;
063
064        /**
065         * Platform font name.
066         */
067        @pref @writeExplicitly
068        public String name = "";
069
070        /**
071         * File name.
072         */
073        @pref @writeExplicitly
074        public String file = "";
075
076        /**
077         * Constructs a new {@code FontEntry}.
078         */
079        public FontEntry() {
080        }
081
082        /**
083         * Constructs a new {@code FontEntry}.
084         * @param charset The character subset. Basically a free identifier, but should be unique
085         * @param name Platform font name
086         * @param file File name
087         */
088        public FontEntry(String charset, String name, String file) {
089            this.charset = charset;
090            this.name = name;
091            this.file = file;
092        }
093    }
094
095    private String osDescription;
096
097    @Override
098    public void preStartupHook() {
099    }
100
101    @Override
102    public void afterPrefStartupHook() {
103    }
104
105    @Override
106    public void startupHook() {
107    }
108
109    @Override
110    public void openUrl(String url) throws IOException {
111        for (String program : Main.pref.getCollection("browser.unix",
112                Arrays.asList("xdg-open", "#DESKTOP#", "$BROWSER", "gnome-open", "kfmclient openURL", "firefox"))) {
113            try {
114                if ("#DESKTOP#".equals(program)) {
115                    Desktop.getDesktop().browse(new URI(url));
116                } else if (program.startsWith("$")) {
117                    program = System.getenv().get(program.substring(1));
118                    Runtime.getRuntime().exec(new String[]{program, url});
119                } else {
120                    Runtime.getRuntime().exec(new String[]{program, url});
121                }
122                return;
123            } catch (IOException | URISyntaxException e) {
124                Main.warn(e);
125            }
126        }
127    }
128
129    @Override
130    public void initSystemShortcuts() {
131        // TODO: Insert system shortcuts here. See Windows and especially OSX to see how to.
132        for(int i = KeyEvent.VK_F1; i <= KeyEvent.VK_F12; ++i)
133            Shortcut.registerSystemShortcut("screen:toogle"+i, tr("reserved"), i, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK).setAutomatic();
134        Shortcut.registerSystemShortcut("system:reset", tr("reserved"), KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK).setAutomatic();
135        Shortcut.registerSystemShortcut("system:resetX", tr("reserved"), KeyEvent.VK_BACK_SPACE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK).setAutomatic();
136    }
137
138    /**
139     * This should work for all platforms. Yeah, should.
140     * See PlatformHook.java for a list of reasons why
141     * this is implemented here...
142     */
143    @Override
144    public String makeTooltip(String name, Shortcut sc) {
145        String result = "";
146        result += "<html>";
147        result += name;
148        if (sc != null && sc.getKeyText().length() != 0) {
149            result += " ";
150            result += "<font size='-2'>";
151            result += "("+sc.getKeyText()+")";
152            result += "</font>";
153        }
154        result += "&nbsp;</html>";
155        return result;
156    }
157
158    @Override
159    public String getDefaultStyle() {
160        return "javax.swing.plaf.metal.MetalLookAndFeel";
161    }
162
163    @Override
164    public boolean canFullscreen() {
165        return !GraphicsEnvironment.isHeadless() &&
166                GraphicsEnvironment.getLocalGraphicsEnvironment()
167        .getDefaultScreenDevice().isFullScreenSupported();
168    }
169
170    @Override
171    public boolean rename(File from, File to) {
172        return from.renameTo(to);
173    }
174
175    /**
176     * Determines if the JVM is OpenJDK-based.
177     * @return {@code true} if {@code java.home} contains "openjdk", {@code false} otherwise
178     * @since 6951
179     */
180    public static boolean isOpenJDK() {
181        String javaHome = System.getProperty("java.home");
182        return javaHome != null && javaHome.contains("openjdk");
183    }
184
185    /**
186     * Get the package name including detailed version.
187     * @param packageNames The possible package names (when a package can have different names on different distributions)
188     * @return The package name and package version if it can be identified, null otherwise
189     * @since 7314
190     */
191    public static String getPackageDetails(String ... packageNames) {
192        try {
193            boolean dpkg = Files.exists(Paths.get("/usr/bin/dpkg-query"));
194            boolean eque = Files.exists(Paths.get("/usr/bin/equery"));
195            boolean rpm  = Files.exists(Paths.get("/bin/rpm"));
196            if (dpkg || rpm || eque) {
197                for (String packageName : packageNames) {
198                    String[] args = null;
199                    if (dpkg) {
200                        args = new String[] {"dpkg-query", "--show", "--showformat", "${Architecture}-${Version}", packageName};
201                    } else if (eque) {
202                        args = new String[] {"equery", "-q", "list", "-e", "--format=$fullversion", packageName};
203                    } else {
204                        args = new String[] {"rpm", "-q", "--qf", "%{arch}-%{version}", packageName};
205                    }
206                    String version = Utils.execOutput(Arrays.asList(args));
207                    if (version != null && !version.contains("not installed")) {
208                        return packageName + ":" + version;
209                    }
210                }
211            }
212        } catch (IOException e) {
213            Main.warn(e);
214        }
215        return null;
216    }
217
218    /**
219     * Get the Java package name including detailed version.
220     *
221     * Some Java bugs are specific to a certain security update, so in addition
222     * to the Java version, we also need the exact package version.
223     *
224     * @return The package name and package version if it can be identified, null otherwise
225     */
226    public String getJavaPackageDetails() {
227        String home = System.getProperty("java.home");
228        if (home.contains("java-7-openjdk") || home.contains("java-1.7.0-openjdk")) {
229            return getPackageDetails("openjdk-7-jre", "java-1_7_0-openjdk", "java-1.7.0-openjdk");
230        } else if (home.contains("icedtea")) {
231            return getPackageDetails("icedtea-bin");
232        } else if (home.contains("oracle")) {
233            return getPackageDetails("oracle-jdk-bin", "oracle-jre-bin");
234        }
235        return null;
236    }
237
238    /**
239     * Get the Web Start package name including detailed version.
240     *
241     * OpenJDK packages are shipped with icedtea-web package,
242     * but its version generally does not match main java package version.
243     *
244     * Simply return {@code null} if there's no separate package for Java WebStart.
245     *
246     * @return The package name and package version if it can be identified, null otherwise
247     */
248    public String getWebStartPackageDetails() {
249        if (isOpenJDK()) {
250            return getPackageDetails("icedtea-netx", "icedtea-web");
251        }
252        return null;
253    }
254
255    protected String buildOSDescription() {
256        String osName = System.getProperty("os.name");
257        if ("Linux".equalsIgnoreCase(osName)) {
258            try {
259                // Try lsb_release (only available on LSB-compliant Linux systems, see https://www.linuxbase.org/lsb-cert/productdir.php?by_prod )
260                Process p = Runtime.getRuntime().exec("lsb_release -ds");
261                try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
262                    String line = Utils.strip(input.readLine());
263                    if (line != null && !line.isEmpty()) {
264                        line = line.replaceAll("\"+","");
265                        line = line.replaceAll("NAME=",""); // strange code for some Gentoo's
266                        if(line.startsWith("Linux ")) // e.g. Linux Mint
267                            return line;
268                        else if(!line.isEmpty())
269                            return "Linux " + line;
270                    }
271                }
272            } catch (IOException e) {
273                // Non LSB-compliant Linux system. List of common fallback release files: http://linuxmafia.com/faq/Admin/release-files.html
274                for (LinuxReleaseInfo info : new LinuxReleaseInfo[]{
275                        new LinuxReleaseInfo("/etc/lsb-release", "DISTRIB_DESCRIPTION", "DISTRIB_ID", "DISTRIB_RELEASE"),
276                        new LinuxReleaseInfo("/etc/os-release", "PRETTY_NAME", "NAME", "VERSION"),
277                        new LinuxReleaseInfo("/etc/arch-release"),
278                        new LinuxReleaseInfo("/etc/debian_version", "Debian GNU/Linux "),
279                        new LinuxReleaseInfo("/etc/fedora-release"),
280                        new LinuxReleaseInfo("/etc/gentoo-release"),
281                        new LinuxReleaseInfo("/etc/redhat-release"),
282                        new LinuxReleaseInfo("/etc/SuSE-release")
283                }) {
284                    String description = info.extractDescription();
285                    if (description != null && !description.isEmpty()) {
286                        return "Linux " + description;
287                    }
288                }
289            }
290        }
291        return osName;
292    }
293
294    @Override
295    public String getOSDescription() {
296        if (osDescription == null) {
297            osDescription = buildOSDescription();
298        }
299        return osDescription;
300    }
301
302    protected static class LinuxReleaseInfo {
303        private final String path;
304        private final String descriptionField;
305        private final String idField;
306        private final String releaseField;
307        private final boolean plainText;
308        private final String prefix;
309
310        public LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField) {
311            this(path, descriptionField, idField, releaseField, false, null);
312        }
313
314        public LinuxReleaseInfo(String path) {
315            this(path, null, null, null, true, null);
316        }
317
318        public LinuxReleaseInfo(String path, String prefix) {
319            this(path, null, null, null, true, prefix);
320        }
321
322        private LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField, boolean plainText, String prefix) {
323            this.path = path;
324            this.descriptionField = descriptionField;
325            this.idField = idField;
326            this.releaseField = releaseField;
327            this.plainText = plainText;
328            this.prefix = prefix;
329        }
330
331        @Override public String toString() {
332            return "ReleaseInfo [path=" + path + ", descriptionField=" + descriptionField +
333                    ", idField=" + idField + ", releaseField=" + releaseField + "]";
334        }
335
336        /**
337         * Extracts OS detailed information from a Linux release file (/etc/xxx-release)
338         * @return The OS detailed information, or {@code null}
339         */
340        public String extractDescription() {
341            String result = null;
342            if (path != null) {
343                Path p = Paths.get(path);
344                if (Files.exists(p)) {
345                    try (BufferedReader reader = Files.newBufferedReader(p, StandardCharsets.UTF_8)) {
346                        String id = null;
347                        String release = null;
348                        String line;
349                        while (result == null && (line = reader.readLine()) != null) {
350                            if (line.contains("=")) {
351                                String[] tokens = line.split("=");
352                                if (tokens.length >= 2) {
353                                    // Description, if available, contains exactly what we need
354                                    if (descriptionField != null && descriptionField.equalsIgnoreCase(tokens[0])) {
355                                        result = Utils.strip(tokens[1]);
356                                    } else if (idField != null && idField.equalsIgnoreCase(tokens[0])) {
357                                        id = Utils.strip(tokens[1]);
358                                    } else if (releaseField != null && releaseField.equalsIgnoreCase(tokens[0])) {
359                                        release = Utils.strip(tokens[1]);
360                                    }
361                                }
362                            } else if (plainText && !line.isEmpty()) {
363                                // Files composed of a single line
364                                result = Utils.strip(line);
365                            }
366                        }
367                        // If no description has been found, try to rebuild it with "id" + "release" (i.e. "name" + "version")
368                        if (result == null && id != null && release != null) {
369                            result = id + " " + release;
370                        }
371                    } catch (IOException e) {
372                        // Ignore
373                    }
374                }
375            }
376            // Append prefix if any
377            if (result != null && !result.isEmpty() && prefix != null && !prefix.isEmpty()) {
378                result = prefix + result;
379            }
380            if(result != null)
381                result = result.replaceAll("\"+","");
382            return result;
383        }
384    }
385
386    protected void askUpdateJava(String version) {
387        askUpdateJava(version, "https://www.java.com/download");
388    }
389
390    // Method kept because strings have already been translated. To enable for Java 8 migration somewhere in 2016
391    protected void askUpdateJava(final String version, final String url) {
392        GuiHelper.runInEDTAndWait(new Runnable() {
393            @Override
394            public void run() {
395                ExtendedDialog ed = new ExtendedDialog(
396                        Main.parent,
397                        tr("Outdated Java version"),
398                        new String[]{tr("Update Java"), tr("Cancel")});
399                // Check if the dialog has not already been permanently hidden by user
400                if (!ed.toggleEnable("askUpdateJava8").toggleCheckState()) {
401                    ed.setButtonIcons(new String[]{"java", "cancel"}).setCancelButton(2);
402                    ed.setMinimumSize(new Dimension(480, 300));
403                    ed.setIcon(JOptionPane.WARNING_MESSAGE);
404                    String content = tr("You are running version {0} of Java.", "<b>"+version+"</b>")+"<br><br>";
405                    if ("Sun Microsystems Inc.".equals(System.getProperty("java.vendor")) && !isOpenJDK()) {
406                        content += "<b>"+tr("This version is no longer supported by {0} since {1} and is not recommended for use.",
407                                "Oracle", tr("April 2015"))+"</b><br><br>";
408                    }
409                    content += "<b>"+tr("JOSM will soon stop working with this version; we highly recommend you to update to Java {0}.", "8")+"</b><br><br>"+
410                            tr("Would you like to update now ?");
411                    ed.setContent(content);
412
413                    if (ed.showDialog().getValue() == 1) {
414                        try {
415                            openUrl(url);
416                        } catch (IOException e) {
417                            Main.warn(e);
418                        }
419                    }
420                }
421            }
422        });
423    }
424
425    @Override
426    public boolean setupHttpsCertificate(String entryAlias, KeyStore.TrustedCertificateEntry trustedCert)
427            throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
428        // TODO setup HTTPS certificate on Unix systems
429        return false;
430    }
431
432    @Override
433    public File getDefaultCacheDirectory() {
434        return new File(Main.pref.getUserDataDirectory(), "cache");
435    }
436
437    @Override
438    public File getDefaultPrefDirectory() {
439        return new File(System.getProperty("user.home"), ".josm");
440    }
441
442    @Override
443    public File getDefaultUserDataDirectory() {
444        // Use preferences directory by default
445        return Main.pref.getPreferencesDirectory();
446    }
447
448    /**
449     * <p>Add more fallback fonts to the Java runtime, in order to get
450     * support for more scripts.</p>
451     *
452     * <p>The font configuration in Java doesn't include some Indic scripts,
453     * even though MS Windows ships with fonts that cover these unicode
454     * ranges.</p>
455     *
456     * <p>To fix this, the fontconfig.properties template is copied to the JOSM
457     * cache folder. Then, the additional entries are added to the font
458     * configuration. Finally the system property "sun.awt.fontconfig" is set
459     * to the customized fontconfig.properties file.</p>
460     *
461     * <p>This is a crude hack, but better than no font display at all for these
462     * languages.
463     * There is no guarantee, that the template file
464     * ($JAVA_HOME/lib/fontconfig.properties.src) matches the default
465     * configuration (which is in a binary format).
466     * Furthermore, the system property "sun.awt.fontconfig" is undocumented and
467     * may no longer work in future versions of Java.</p>
468     *
469     * <p>Related Java bug: <a href="https://bugs.openjdk.java.net/browse/JDK-8008572">JDK-8008572</a></p>
470     *
471     * @param templateFileName file name of the fontconfig.properties template file
472     */
473    protected void extendFontconfig(String templateFileName) {
474        String customFontconfigFile = Main.pref.get("fontconfig.properties", null);
475        if (customFontconfigFile != null) {
476            Utils.updateSystemProperty("sun.awt.fontconfig", customFontconfigFile);
477            return;
478        }
479        if (!Main.pref.getBoolean("font.extended-unicode", true))
480            return;
481
482        String javaLibPath = System.getProperty("java.home") + File.separator + "lib";
483        Path templateFile = FileSystems.getDefault().getPath(javaLibPath, templateFileName);
484        if (!Files.isReadable(templateFile)) {
485            Main.warn("extended font config - unable to find font config template file "+templateFile.toString());
486            return;
487        }
488        try {
489            Properties props = new Properties();
490            props.load(new FileInputStream(templateFile.toFile()));
491            byte[] content = Files.readAllBytes(templateFile);
492            File cachePath = Main.pref.getCacheDirectory();
493            Path fontconfigFile = cachePath.toPath().resolve("fontconfig.properties");
494            OutputStream os = Files.newOutputStream(fontconfigFile);
495            os.write(content);
496            try (Writer w = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8))) {
497                Collection<FontEntry> extrasPref = Main.pref.getListOfStructs(
498                        "font.extended-unicode.extra-items", getAdditionalFonts(), FontEntry.class);
499                Collection<FontEntry> extras = new ArrayList<>();
500                w.append("\n\n# Added by JOSM to extend unicode coverage of Java font support:\n\n");
501                List<String> allCharSubsets = new ArrayList<>();
502                for (FontEntry entry: extrasPref) {
503                    Collection<String> fontsAvail = getInstalledFonts();
504                    if (fontsAvail != null && fontsAvail.contains(entry.file.toUpperCase())) {
505                        if (!allCharSubsets.contains(entry.charset)) {
506                            allCharSubsets.add(entry.charset);
507                            extras.add(entry);
508                        } else {
509                            Main.trace("extended font config - already registered font for charset ''{0}'' - skipping ''{1}''",
510                                    entry.charset, entry.name);
511                        }
512                    } else {
513                        Main.trace("extended font config - Font ''{0}'' not found on system - skipping", entry.name);
514                    }
515                }
516                for (FontEntry entry: extras) {
517                    allCharSubsets.add(entry.charset);
518                    if ("".equals(entry.name)) {
519                        continue;
520                    }
521                    String key = "allfonts." + entry.charset;
522                    String value = entry.name;
523                    String prevValue = props.getProperty(key);
524                    if (prevValue != null && !prevValue.equals(value)) {
525                        Main.warn("extended font config - overriding ''{0}={1}'' with ''{2}''", key, prevValue, value);
526                    }
527                    w.append(key + "=" + value + "\n");
528                }
529                w.append("\n");
530                for (FontEntry entry: extras) {
531                    if ("".equals(entry.name) || "".equals(entry.file)) {
532                        continue;
533                    }
534                    String key = "filename." + entry.name.replace(" ", "_");
535                    String value = entry.file;
536                    String prevValue = props.getProperty(key);
537                    if (prevValue != null && !prevValue.equals(value)) {
538                        Main.warn("extended font config - overriding ''{0}={1}'' with ''{2}''", key, prevValue, value);
539                    }
540                    w.append(key + "=" + value + "\n");
541                }
542                w.append("\n");
543                String fallback = props.getProperty("sequence.fallback");
544                if (fallback != null) {
545                    w.append("sequence.fallback=" + fallback + "," + Utils.join(",", allCharSubsets) + "\n");
546                } else {
547                    w.append("sequence.fallback=" + Utils.join(",", allCharSubsets) + "\n");
548                }
549            }
550            Utils.updateSystemProperty("sun.awt.fontconfig", fontconfigFile.toString());
551        } catch (IOException ex) {
552            Main.error(ex);
553        }
554    }
555
556    /**
557     * Get a list of fonts that are installed on the system.
558     *
559     * Must be done without triggering the Java Font initialization.
560     * (See {@link #extendFontconfig(java.lang.String)}, have to set system
561     * property first, which is then read by sun.awt.FontConfiguration upon
562     * initialization.)
563     *
564     * @return list of file names
565     */
566    public Collection<String> getInstalledFonts() {
567        throw new UnsupportedOperationException();
568    }
569
570    /**
571     * Get default list of additional fonts to add to the configuration.
572     *
573     * Java will choose thee first font in the list that can render a certain
574     * character.
575     *
576     * @return list of FontEntry objects
577     */
578    public Collection<FontEntry> getAdditionalFonts() {
579        throw new UnsupportedOperationException();
580    }
581}