source: josm/trunk/src/org/openstreetmap/josm/tools/PlatformHookUnixoid.java@ 11117

Last change on this file since 11117 was 11096, checked in by Don-vip, 8 years ago

sonar - squid:S3725 - Java 8's Files.exists should not be used (The Files.exists method has noticeably poor performance in JDK 8, and can slow an application significantly when used to check files that don't actually exist)

  • Property svn:eol-style set to native
File size: 26.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Desktop;
7import java.awt.Dimension;
8import java.awt.GraphicsEnvironment;
9import java.awt.event.KeyEvent;
10import java.io.BufferedReader;
11import java.io.BufferedWriter;
12import java.io.File;
13import java.io.FileInputStream;
14import java.io.IOException;
15import java.io.InputStreamReader;
16import java.io.OutputStream;
17import java.io.OutputStreamWriter;
18import java.io.Writer;
19import java.net.URI;
20import java.net.URISyntaxException;
21import java.nio.charset.StandardCharsets;
22import java.nio.file.FileSystems;
23import java.nio.file.Files;
24import java.nio.file.Path;
25import java.nio.file.Paths;
26import java.security.KeyStore;
27import java.security.KeyStoreException;
28import java.security.NoSuchAlgorithmException;
29import java.security.cert.CertificateException;
30import java.util.ArrayList;
31import java.util.Arrays;
32import java.util.Collection;
33import java.util.List;
34import java.util.Locale;
35import java.util.Properties;
36
37import javax.swing.JOptionPane;
38
39import org.openstreetmap.josm.Main;
40import org.openstreetmap.josm.data.Preferences.pref;
41import org.openstreetmap.josm.data.Preferences.writeExplicitly;
42import org.openstreetmap.josm.gui.ExtendedDialog;
43import org.openstreetmap.josm.gui.util.GuiHelper;
44
45/**
46 * {@code PlatformHook} base implementation.
47 *
48 * Don't write (Main.platform instanceof PlatformHookUnixoid) because other platform
49 * hooks are subclasses of this class.
50 */
51public class PlatformHookUnixoid implements PlatformHook {
52
53 /**
54 * Simple data class to hold information about a font.
55 *
56 * Used for fontconfig.properties files.
57 */
58 public static class FontEntry {
59 /**
60 * The character subset. Basically a free identifier, but should be unique.
61 */
62 @pref
63 public String charset;
64
65 /**
66 * Platform font name.
67 */
68 @pref
69 @writeExplicitly
70 public String name = "";
71
72 /**
73 * File name.
74 */
75 @pref
76 @writeExplicitly
77 public String file = "";
78
79 /**
80 * Constructs a new {@code FontEntry}.
81 */
82 public FontEntry() {
83 }
84
85 /**
86 * Constructs a new {@code FontEntry}.
87 * @param charset The character subset. Basically a free identifier, but should be unique
88 * @param name Platform font name
89 * @param file File name
90 */
91 public FontEntry(String charset, String name, String file) {
92 this.charset = charset;
93 this.name = name;
94 this.file = file;
95 }
96 }
97
98 private String osDescription;
99
100 @Override
101 public void preStartupHook() {
102 // See #12022 - Disable GNOME ATK Java wrapper as it causes a lot of serious trouble
103 if ("org.GNOME.Accessibility.AtkWrapper".equals(System.getProperty("assistive_technologies"))) {
104 System.clearProperty("assistive_technologies");
105 }
106 }
107
108 @Override
109 public void afterPrefStartupHook() {
110 // Do nothing
111 }
112
113 @Override
114 public void startupHook() {
115 // Do nothing
116 }
117
118 @Override
119 public void openUrl(String url) throws IOException {
120 for (String program : Main.pref.getCollection("browser.unix",
121 Arrays.asList("xdg-open", "#DESKTOP#", "$BROWSER", "gnome-open", "kfmclient openURL", "firefox"))) {
122 try {
123 if ("#DESKTOP#".equals(program)) {
124 Desktop.getDesktop().browse(new URI(url));
125 } else if (program.startsWith("$")) {
126 program = System.getenv().get(program.substring(1));
127 Runtime.getRuntime().exec(new String[]{program, url});
128 } else {
129 Runtime.getRuntime().exec(new String[]{program, url});
130 }
131 return;
132 } catch (IOException | URISyntaxException e) {
133 Main.warn(e);
134 }
135 }
136 }
137
138 @Override
139 public void initSystemShortcuts() {
140 // CHECKSTYLE.OFF: LineLength
141 // TODO: Insert system shortcuts here. See Windows and especially OSX to see how to.
142 for (int i = KeyEvent.VK_F1; i <= KeyEvent.VK_F12; ++i) {
143 Shortcut.registerSystemShortcut("screen:toogle"+i, tr("reserved"), i, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK)
144 .setAutomatic();
145 }
146 Shortcut.registerSystemShortcut("system:reset", tr("reserved"), KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK)
147 .setAutomatic();
148 Shortcut.registerSystemShortcut("system:resetX", tr("reserved"), KeyEvent.VK_BACK_SPACE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK)
149 .setAutomatic();
150 // CHECKSTYLE.ON: LineLength
151 }
152
153 /**
154 * This should work for all platforms. Yeah, should.
155 * See PlatformHook.java for a list of reasons why this is implemented here...
156 */
157 @Override
158 public String makeTooltip(String name, Shortcut sc) {
159 StringBuilder result = new StringBuilder();
160 result.append("<html>").append(name);
161 if (sc != null && !sc.getKeyText().isEmpty()) {
162 result.append(" <font size='-2'>(")
163 .append(sc.getKeyText())
164 .append(")</font>");
165 }
166 return result.append("&nbsp;</html>").toString();
167 }
168
169 @Override
170 public String getDefaultStyle() {
171 return "javax.swing.plaf.metal.MetalLookAndFeel";
172 }
173
174 @Override
175 public boolean canFullscreen() {
176 return !GraphicsEnvironment.isHeadless() &&
177 GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().isFullScreenSupported();
178 }
179
180 @Override
181 public boolean rename(File from, File to) {
182 return from.renameTo(to);
183 }
184
185 /**
186 * Determines if the distribution is Debian or Ubuntu, or a derivative.
187 * @return {@code true} if the distribution is Debian, Ubuntu or Mint, {@code false} otherwise
188 */
189 public static boolean isDebianOrUbuntu() {
190 try {
191 String dist = Utils.execOutput(Arrays.asList("lsb_release", "-i", "-s"));
192 return "Debian".equalsIgnoreCase(dist) || "Ubuntu".equalsIgnoreCase(dist) || "Mint".equalsIgnoreCase(dist);
193 } catch (IOException e) {
194 // lsb_release is not available on all Linux systems, so don't log at warning level
195 Main.debug(e);
196 return false;
197 }
198 }
199
200 /**
201 * Determines if the JVM is OpenJDK-based.
202 * @return {@code true} if {@code java.home} contains "openjdk", {@code false} otherwise
203 * @since 6951
204 */
205 public static boolean isOpenJDK() {
206 String javaHome = System.getProperty("java.home");
207 return javaHome != null && javaHome.contains("openjdk");
208 }
209
210 /**
211 * Get the package name including detailed version.
212 * @param packageNames The possible package names (when a package can have different names on different distributions)
213 * @return The package name and package version if it can be identified, null otherwise
214 * @since 7314
215 */
216 public static String getPackageDetails(String ... packageNames) {
217 try {
218 // CHECKSTYLE.OFF: SingleSpaceSeparator
219 boolean dpkg = Paths.get("/usr/bin/dpkg-query").toFile().exists();
220 boolean eque = Paths.get("/usr/bin/equery").toFile().exists();
221 boolean rpm = Paths.get("/bin/rpm").toFile().exists();
222 // CHECKSTYLE.ON: SingleSpaceSeparator
223 if (dpkg || rpm || eque) {
224 for (String packageName : packageNames) {
225 String[] args;
226 if (dpkg) {
227 args = new String[] {"dpkg-query", "--show", "--showformat", "${Architecture}-${Version}", packageName};
228 } else if (eque) {
229 args = new String[] {"equery", "-q", "list", "-e", "--format=$fullversion", packageName};
230 } else {
231 args = new String[] {"rpm", "-q", "--qf", "%{arch}-%{version}", packageName};
232 }
233 String version = Utils.execOutput(Arrays.asList(args));
234 if (version != null && !version.contains("not installed")) {
235 return packageName + ':' + version;
236 }
237 }
238 }
239 } catch (IOException e) {
240 Main.warn(e);
241 }
242 return null;
243 }
244
245 /**
246 * Get the Java package name including detailed version.
247 *
248 * Some Java bugs are specific to a certain security update, so in addition
249 * to the Java version, we also need the exact package version.
250 *
251 * @return The package name and package version if it can be identified, null otherwise
252 */
253 public String getJavaPackageDetails() {
254 String home = System.getProperty("java.home");
255 if (home.contains("java-8-openjdk") || home.contains("java-1.8.0-openjdk")) {
256 return getPackageDetails("openjdk-8-jre", "java-1_8_0-openjdk", "java-1.8.0-openjdk");
257 } else if (home.contains("java-9-openjdk") || home.contains("java-1.9.0-openjdk")) {
258 return getPackageDetails("openjdk-9-jre", "java-1_9_0-openjdk", "java-1.9.0-openjdk");
259 } else if (home.contains("icedtea")) {
260 return getPackageDetails("icedtea-bin");
261 } else if (home.contains("oracle")) {
262 return getPackageDetails("oracle-jdk-bin", "oracle-jre-bin");
263 }
264 return null;
265 }
266
267 /**
268 * Get the Web Start package name including detailed version.
269 *
270 * OpenJDK packages are shipped with icedtea-web package,
271 * but its version generally does not match main java package version.
272 *
273 * Simply return {@code null} if there's no separate package for Java WebStart.
274 *
275 * @return The package name and package version if it can be identified, null otherwise
276 */
277 public String getWebStartPackageDetails() {
278 if (isOpenJDK()) {
279 return getPackageDetails("icedtea-netx", "icedtea-web");
280 }
281 return null;
282 }
283
284 /**
285 * Get the Gnome ATK wrapper package name including detailed version.
286 *
287 * Debian and Ubuntu derivatives come with a pre-enabled accessibility software
288 * completely buggy that makes Swing crash in a lot of different ways.
289 *
290 * Simply return {@code null} if it's not found.
291 *
292 * @return The package name and package version if it can be identified, null otherwise
293 */
294 public String getAtkWrapperPackageDetails() {
295 if (isOpenJDK() && isDebianOrUbuntu()) {
296 return getPackageDetails("libatk-wrapper-java");
297 }
298 return null;
299 }
300
301 protected String buildOSDescription() {
302 String osName = System.getProperty("os.name");
303 if ("Linux".equalsIgnoreCase(osName)) {
304 try {
305 // Try lsb_release (only available on LSB-compliant Linux systems,
306 // see https://www.linuxbase.org/lsb-cert/productdir.php?by_prod )
307 Process p = Runtime.getRuntime().exec("lsb_release -ds");
308 try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
309 String line = Utils.strip(input.readLine());
310 if (line != null && !line.isEmpty()) {
311 line = line.replaceAll("\"+", "");
312 line = line.replaceAll("NAME=", ""); // strange code for some Gentoo's
313 if (line.startsWith("Linux ")) // e.g. Linux Mint
314 return line;
315 else if (!line.isEmpty())
316 return "Linux " + line;
317 }
318 }
319 } catch (IOException e) {
320 Main.debug(e);
321 // Non LSB-compliant Linux system. List of common fallback release files: http://linuxmafia.com/faq/Admin/release-files.html
322 for (LinuxReleaseInfo info : new LinuxReleaseInfo[]{
323 new LinuxReleaseInfo("/etc/lsb-release", "DISTRIB_DESCRIPTION", "DISTRIB_ID", "DISTRIB_RELEASE"),
324 new LinuxReleaseInfo("/etc/os-release", "PRETTY_NAME", "NAME", "VERSION"),
325 new LinuxReleaseInfo("/etc/arch-release"),
326 new LinuxReleaseInfo("/etc/debian_version", "Debian GNU/Linux "),
327 new LinuxReleaseInfo("/etc/fedora-release"),
328 new LinuxReleaseInfo("/etc/gentoo-release"),
329 new LinuxReleaseInfo("/etc/redhat-release"),
330 new LinuxReleaseInfo("/etc/SuSE-release")
331 }) {
332 String description = info.extractDescription();
333 if (description != null && !description.isEmpty()) {
334 return "Linux " + description;
335 }
336 }
337 }
338 }
339 return osName;
340 }
341
342 @Override
343 public String getOSDescription() {
344 if (osDescription == null) {
345 osDescription = buildOSDescription();
346 }
347 return osDescription;
348 }
349
350 protected static class LinuxReleaseInfo {
351 private final String path;
352 private final String descriptionField;
353 private final String idField;
354 private final String releaseField;
355 private final boolean plainText;
356 private final String prefix;
357
358 public LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField) {
359 this(path, descriptionField, idField, releaseField, false, null);
360 }
361
362 public LinuxReleaseInfo(String path) {
363 this(path, null, null, null, true, null);
364 }
365
366 public LinuxReleaseInfo(String path, String prefix) {
367 this(path, null, null, null, true, prefix);
368 }
369
370 private LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField, boolean plainText, String prefix) {
371 this.path = path;
372 this.descriptionField = descriptionField;
373 this.idField = idField;
374 this.releaseField = releaseField;
375 this.plainText = plainText;
376 this.prefix = prefix;
377 }
378
379 @Override public String toString() {
380 return "ReleaseInfo [path=" + path + ", descriptionField=" + descriptionField +
381 ", idField=" + idField + ", releaseField=" + releaseField + ']';
382 }
383
384 /**
385 * Extracts OS detailed information from a Linux release file (/etc/xxx-release)
386 * @return The OS detailed information, or {@code null}
387 */
388 public String extractDescription() {
389 String result = null;
390 if (path != null) {
391 Path p = Paths.get(path);
392 if (p.toFile().exists()) {
393 try (BufferedReader reader = Files.newBufferedReader(p, StandardCharsets.UTF_8)) {
394 String id = null;
395 String release = null;
396 String line;
397 while (result == null && (line = reader.readLine()) != null) {
398 if (line.contains("=")) {
399 String[] tokens = line.split("=");
400 if (tokens.length >= 2) {
401 // Description, if available, contains exactly what we need
402 if (descriptionField != null && descriptionField.equalsIgnoreCase(tokens[0])) {
403 result = Utils.strip(tokens[1]);
404 } else if (idField != null && idField.equalsIgnoreCase(tokens[0])) {
405 id = Utils.strip(tokens[1]);
406 } else if (releaseField != null && releaseField.equalsIgnoreCase(tokens[0])) {
407 release = Utils.strip(tokens[1]);
408 }
409 }
410 } else if (plainText && !line.isEmpty()) {
411 // Files composed of a single line
412 result = Utils.strip(line);
413 }
414 }
415 // If no description has been found, try to rebuild it with "id" + "release" (i.e. "name" + "version")
416 if (result == null && id != null && release != null) {
417 result = id + ' ' + release;
418 }
419 } catch (IOException e) {
420 // Ignore
421 Main.trace(e);
422 }
423 }
424 }
425 // Append prefix if any
426 if (result != null && !result.isEmpty() && prefix != null && !prefix.isEmpty()) {
427 result = prefix + result;
428 }
429 if (result != null)
430 result = result.replaceAll("\"+", "");
431 return result;
432 }
433 }
434
435 // Method unused, but kept for translation already done. To reuse during Java 9 migration
436 protected void askUpdateJava(final String version, final String url) {
437 GuiHelper.runInEDTAndWait(() -> {
438 ExtendedDialog ed = new ExtendedDialog(
439 Main.parent,
440 tr("Outdated Java version"),
441 new String[]{tr("OK"), tr("Update Java"), tr("Cancel")});
442 // Check if the dialog has not already been permanently hidden by user
443 if (!ed.toggleEnable("askUpdateJava9").toggleCheckState()) {
444 ed.setButtonIcons(new String[]{"ok", "java", "cancel"}).setCancelButton(3);
445 ed.setMinimumSize(new Dimension(480, 300));
446 ed.setIcon(JOptionPane.WARNING_MESSAGE);
447 StringBuilder content = new StringBuilder(tr("You are running version {0} of Java.", "<b>"+version+"</b>"))
448 .append("<br><br>");
449 if ("Sun Microsystems Inc.".equals(System.getProperty("java.vendor")) && !isOpenJDK()) {
450 content.append("<b>").append(tr("This version is no longer supported by {0} since {1} and is not recommended for use.",
451 "Oracle", tr("April 2015"))).append("</b><br><br>"); // TODO: change date once Java 8 EOL is announced
452 }
453 content.append("<b>")
454 .append(tr("JOSM will soon stop working with this version; we highly recommend you to update to Java {0}.", "8"))
455 .append("</b><br><br>")
456 .append(tr("Would you like to update now ?"));
457 ed.setContent(content.toString());
458
459 if (ed.showDialog().getValue() == 2) {
460 try {
461 openUrl(url);
462 } catch (IOException e) {
463 Main.warn(e);
464 }
465 }
466 }
467 });
468 }
469
470 @Override
471 public boolean setupHttpsCertificate(String entryAlias, KeyStore.TrustedCertificateEntry trustedCert)
472 throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
473 // TODO setup HTTPS certificate on Unix systems
474 return false;
475 }
476
477 @Override
478 public File getDefaultCacheDirectory() {
479 return new File(Main.pref.getUserDataDirectory(), "cache");
480 }
481
482 @Override
483 public File getDefaultPrefDirectory() {
484 return new File(System.getProperty("user.home"), ".josm");
485 }
486
487 @Override
488 public File getDefaultUserDataDirectory() {
489 // Use preferences directory by default
490 return Main.pref.getPreferencesDirectory();
491 }
492
493 /**
494 * <p>Add more fallback fonts to the Java runtime, in order to get
495 * support for more scripts.</p>
496 *
497 * <p>The font configuration in Java doesn't include some Indic scripts,
498 * even though MS Windows ships with fonts that cover these unicode ranges.</p>
499 *
500 * <p>To fix this, the fontconfig.properties template is copied to the JOSM
501 * cache folder. Then, the additional entries are added to the font
502 * configuration. Finally the system property "sun.awt.fontconfig" is set
503 * to the customized fontconfig.properties file.</p>
504 *
505 * <p>This is a crude hack, but better than no font display at all for these languages.
506 * There is no guarantee, that the template file
507 * ($JAVA_HOME/lib/fontconfig.properties.src) matches the default
508 * configuration (which is in a binary format).
509 * Furthermore, the system property "sun.awt.fontconfig" is undocumented and
510 * may no longer work in future versions of Java.</p>
511 *
512 * <p>Related Java bug: <a href="https://bugs.openjdk.java.net/browse/JDK-8008572">JDK-8008572</a></p>
513 *
514 * @param templateFileName file name of the fontconfig.properties template file
515 */
516 protected void extendFontconfig(String templateFileName) {
517 String customFontconfigFile = Main.pref.get("fontconfig.properties", null);
518 if (customFontconfigFile != null) {
519 Utils.updateSystemProperty("sun.awt.fontconfig", customFontconfigFile);
520 return;
521 }
522 if (!Main.pref.getBoolean("font.extended-unicode", true))
523 return;
524
525 String javaLibPath = System.getProperty("java.home") + File.separator + "lib";
526 Path templateFile = FileSystems.getDefault().getPath(javaLibPath, templateFileName);
527 if (!Files.isReadable(templateFile)) {
528 Main.warn("extended font config - unable to find font config template file "+templateFile.toString());
529 return;
530 }
531 try (FileInputStream fis = new FileInputStream(templateFile.toFile())) {
532 Properties props = new Properties();
533 props.load(fis);
534 byte[] content = Files.readAllBytes(templateFile);
535 File cachePath = Main.pref.getCacheDirectory();
536 Path fontconfigFile = cachePath.toPath().resolve("fontconfig.properties");
537 OutputStream os = Files.newOutputStream(fontconfigFile);
538 os.write(content);
539 try (Writer w = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8))) {
540 Collection<FontEntry> extrasPref = Main.pref.getListOfStructs(
541 "font.extended-unicode.extra-items", getAdditionalFonts(), FontEntry.class);
542 Collection<FontEntry> extras = new ArrayList<>();
543 w.append("\n\n# Added by JOSM to extend unicode coverage of Java font support:\n\n");
544 List<String> allCharSubsets = new ArrayList<>();
545 for (FontEntry entry: extrasPref) {
546 Collection<String> fontsAvail = getInstalledFonts();
547 if (fontsAvail != null && fontsAvail.contains(entry.file.toUpperCase(Locale.ENGLISH))) {
548 if (!allCharSubsets.contains(entry.charset)) {
549 allCharSubsets.add(entry.charset);
550 extras.add(entry);
551 } else {
552 Main.trace("extended font config - already registered font for charset ''{0}'' - skipping ''{1}''",
553 entry.charset, entry.name);
554 }
555 } else {
556 Main.trace("extended font config - Font ''{0}'' not found on system - skipping", entry.name);
557 }
558 }
559 for (FontEntry entry: extras) {
560 allCharSubsets.add(entry.charset);
561 if ("".equals(entry.name)) {
562 continue;
563 }
564 String key = "allfonts." + entry.charset;
565 String value = entry.name;
566 String prevValue = props.getProperty(key);
567 if (prevValue != null && !prevValue.equals(value)) {
568 Main.warn("extended font config - overriding ''{0}={1}'' with ''{2}''", key, prevValue, value);
569 }
570 w.append(key + '=' + value + '\n');
571 }
572 w.append('\n');
573 for (FontEntry entry: extras) {
574 if ("".equals(entry.name) || "".equals(entry.file)) {
575 continue;
576 }
577 String key = "filename." + entry.name.replace(' ', '_');
578 String value = entry.file;
579 String prevValue = props.getProperty(key);
580 if (prevValue != null && !prevValue.equals(value)) {
581 Main.warn("extended font config - overriding ''{0}={1}'' with ''{2}''", key, prevValue, value);
582 }
583 w.append(key + '=' + value + '\n');
584 }
585 w.append('\n');
586 String fallback = props.getProperty("sequence.fallback");
587 if (fallback != null) {
588 w.append("sequence.fallback=" + fallback + ',' + Utils.join(",", allCharSubsets) + '\n');
589 } else {
590 w.append("sequence.fallback=" + Utils.join(",", allCharSubsets) + '\n');
591 }
592 }
593 Utils.updateSystemProperty("sun.awt.fontconfig", fontconfigFile.toString());
594 } catch (IOException ex) {
595 Main.error(ex);
596 }
597 }
598
599 /**
600 * Get a list of fonts that are installed on the system.
601 *
602 * Must be done without triggering the Java Font initialization.
603 * (See {@link #extendFontconfig(java.lang.String)}, have to set system
604 * property first, which is then read by sun.awt.FontConfiguration upon initialization.)
605 *
606 * @return list of file names
607 */
608 public Collection<String> getInstalledFonts() {
609 throw new UnsupportedOperationException();
610 }
611
612 /**
613 * Get default list of additional fonts to add to the configuration.
614 *
615 * Java will choose thee first font in the list that can render a certain character.
616 *
617 * @return list of FontEntry objects
618 */
619 public Collection<FontEntry> getAdditionalFonts() {
620 throw new UnsupportedOperationException();
621 }
622}
Note: See TracBrowser for help on using the repository browser.