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