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 Main.warn(e); 212 return false; 213 } 214 } 215 216 /** 217 * Determines if the JVM is OpenJDK-based. 218 * @return {@code true} if {@code java.home} contains "openjdk", {@code false} otherwise 219 * @since 6951 220 */ 221 public static boolean isOpenJDK() { 222 String javaHome = System.getProperty("java.home"); 223 return javaHome != null && javaHome.contains("openjdk"); 224 } 225 226 /** 227 * Get the package name including detailed version. 228 * @param packageNames The possible package names (when a package can have different names on different distributions) 229 * @return The package name and package version if it can be identified, null otherwise 230 * @since 7314 231 */ 232 public static String getPackageDetails(String ... packageNames) { 233 try { 234 boolean dpkg = Files.exists(Paths.get("/usr/bin/dpkg-query")); 235 boolean eque = Files.exists(Paths.get("/usr/bin/equery")); 236 boolean rpm = Files.exists(Paths.get("/bin/rpm")); 237 if (dpkg || rpm || eque) { 238 for (String packageName : packageNames) { 239 String[] args; 240 if (dpkg) { 241 args = new String[] {"dpkg-query", "--show", "--showformat", "${Architecture}-${Version}", packageName}; 242 } else if (eque) { 243 args = new String[] {"equery", "-q", "list", "-e", "--format=$fullversion", packageName}; 244 } else { 245 args = new String[] {"rpm", "-q", "--qf", "%{arch}-%{version}", packageName}; 246 } 247 String version = Utils.execOutput(Arrays.asList(args)); 248 if (version != null && !version.contains("not installed")) { 249 return packageName + ':' + version; 250 } 251 } 252 } 253 } catch (IOException e) { 254 Main.warn(e); 255 } 256 return null; 257 } 258 259 /** 260 * Get the Java package name including detailed version. 261 * 262 * Some Java bugs are specific to a certain security update, so in addition 263 * to the Java version, we also need the exact package version. 264 * 265 * @return The package name and package version if it can be identified, null otherwise 266 */ 267 public String getJavaPackageDetails() { 268 String home = System.getProperty("java.home"); 269 if (home.contains("java-7-openjdk") || home.contains("java-1.7.0-openjdk")) { 270 return getPackageDetails("openjdk-7-jre", "java-1_7_0-openjdk", "java-1.7.0-openjdk"); 271 } else if (home.contains("icedtea")) { 272 return getPackageDetails("icedtea-bin"); 273 } else if (home.contains("oracle")) { 274 return getPackageDetails("oracle-jdk-bin", "oracle-jre-bin"); 275 } 276 return null; 277 } 278 279 /** 280 * Get the Web Start package name including detailed version. 281 * 282 * OpenJDK packages are shipped with icedtea-web package, 283 * but its version generally does not match main java package version. 284 * 285 * Simply return {@code null} if there's no separate package for Java WebStart. 286 * 287 * @return The package name and package version if it can be identified, null otherwise 288 */ 289 public String getWebStartPackageDetails() { 290 if (isOpenJDK()) { 291 return getPackageDetails("icedtea-netx", "icedtea-web"); 292 } 293 return null; 294 } 295 296 protected String buildOSDescription() { 297 String osName = System.getProperty("os.name"); 298 if ("Linux".equalsIgnoreCase(osName)) { 299 try { 300 // Try lsb_release (only available on LSB-compliant Linux systems, 301 // see https://www.linuxbase.org/lsb-cert/productdir.php?by_prod ) 302 Process p = Runtime.getRuntime().exec("lsb_release -ds"); 303 try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { 304 String line = Utils.strip(input.readLine()); 305 if (line != null && !line.isEmpty()) { 306 line = line.replaceAll("\"+", ""); 307 line = line.replaceAll("NAME=", ""); // strange code for some Gentoo's 308 if (line.startsWith("Linux ")) // e.g. Linux Mint 309 return line; 310 else if (!line.isEmpty()) 311 return "Linux " + line; 312 } 313 } 314 } catch (IOException e) { 315 // Non LSB-compliant Linux system. List of common fallback release files: http://linuxmafia.com/faq/Admin/release-files.html 316 for (LinuxReleaseInfo info : new LinuxReleaseInfo[]{ 317 new LinuxReleaseInfo("/etc/lsb-release", "DISTRIB_DESCRIPTION", "DISTRIB_ID", "DISTRIB_RELEASE"), 318 new LinuxReleaseInfo("/etc/os-release", "PRETTY_NAME", "NAME", "VERSION"), 319 new LinuxReleaseInfo("/etc/arch-release"), 320 new LinuxReleaseInfo("/etc/debian_version", "Debian GNU/Linux "), 321 new LinuxReleaseInfo("/etc/fedora-release"), 322 new LinuxReleaseInfo("/etc/gentoo-release"), 323 new LinuxReleaseInfo("/etc/redhat-release"), 324 new LinuxReleaseInfo("/etc/SuSE-release") 325 }) { 326 String description = info.extractDescription(); 327 if (description != null && !description.isEmpty()) { 328 return "Linux " + description; 329 } 330 } 331 } 332 } 333 return osName; 334 } 335 336 @Override 337 public String getOSDescription() { 338 if (osDescription == null) { 339 osDescription = buildOSDescription(); 340 } 341 return osDescription; 342 } 343 344 protected static class LinuxReleaseInfo { 345 private final String path; 346 private final String descriptionField; 347 private final String idField; 348 private final String releaseField; 349 private final boolean plainText; 350 private final String prefix; 351 352 public LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField) { 353 this(path, descriptionField, idField, releaseField, false, null); 354 } 355 356 public LinuxReleaseInfo(String path) { 357 this(path, null, null, null, true, null); 358 } 359 360 public LinuxReleaseInfo(String path, String prefix) { 361 this(path, null, null, null, true, prefix); 362 } 363 364 private LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField, boolean plainText, String prefix) { 365 this.path = path; 366 this.descriptionField = descriptionField; 367 this.idField = idField; 368 this.releaseField = releaseField; 369 this.plainText = plainText; 370 this.prefix = prefix; 371 } 372 373 @Override public String toString() { 374 return "ReleaseInfo [path=" + path + ", descriptionField=" + descriptionField + 375 ", idField=" + idField + ", releaseField=" + releaseField + ']'; 376 } 377 378 /** 379 * Extracts OS detailed information from a Linux release file (/etc/xxx-release) 380 * @return The OS detailed information, or {@code null} 381 */ 382 public String extractDescription() { 383 String result = null; 384 if (path != null) { 385 Path p = Paths.get(path); 386 if (Files.exists(p)) { 387 try (BufferedReader reader = Files.newBufferedReader(p, StandardCharsets.UTF_8)) { 388 String id = null; 389 String release = null; 390 String line; 391 while (result == null && (line = reader.readLine()) != null) { 392 if (line.contains("=")) { 393 String[] tokens = line.split("="); 394 if (tokens.length >= 2) { 395 // Description, if available, contains exactly what we need 396 if (descriptionField != null && descriptionField.equalsIgnoreCase(tokens[0])) { 397 result = Utils.strip(tokens[1]); 398 } else if (idField != null && idField.equalsIgnoreCase(tokens[0])) { 399 id = Utils.strip(tokens[1]); 400 } else if (releaseField != null && releaseField.equalsIgnoreCase(tokens[0])) { 401 release = Utils.strip(tokens[1]); 402 } 403 } 404 } else if (plainText && !line.isEmpty()) { 405 // Files composed of a single line 406 result = Utils.strip(line); 407 } 408 } 409 // If no description has been found, try to rebuild it with "id" + "release" (i.e. "name" + "version") 410 if (result == null && id != null && release != null) { 411 result = id + ' ' + release; 412 } 413 } catch (IOException e) { 414 // Ignore 415 if (Main.isTraceEnabled()) { 416 Main.trace(e.getMessage()); 417 } 418 } 419 } 420 } 421 // Append prefix if any 422 if (result != null && !result.isEmpty() && prefix != null && !prefix.isEmpty()) { 423 result = prefix + result; 424 } 425 if (result != null) 426 result = result.replaceAll("\"+", ""); 427 return result; 428 } 429 } 430 431 protected void askUpdateJava(String version) { 432 if (!GraphicsEnvironment.isHeadless()) { 433 askUpdateJava(version, "https://www.java.com/download"); 434 } 435 } 436 437 protected void askUpdateJava(final String version, final String url) { 438 GuiHelper.runInEDTAndWait(new Runnable() { 439 @Override 440 public void run() { 441 ExtendedDialog ed = new ExtendedDialog( 442 Main.parent, 443 tr("Outdated Java version"), 444 new String[]{tr("OK"), tr("Update Java"), tr("Cancel")}); 445 // Check if the dialog has not already been permanently hidden by user 446 if (!ed.toggleEnable("askUpdateJava8").toggleCheckState()) { 447 ed.setButtonIcons(new String[]{"ok", "java", "cancel"}).setCancelButton(3); 448 ed.setMinimumSize(new Dimension(480, 300)); 449 ed.setIcon(JOptionPane.WARNING_MESSAGE); 450 StringBuilder content = new StringBuilder(tr("You are running version {0} of Java.", "<b>"+version+"</b>")) 451 .append("<br><br>"); 452 if ("Sun Microsystems Inc.".equals(System.getProperty("java.vendor")) && !isOpenJDK()) { 453 content.append("<b>").append(tr("This version is no longer supported by {0} since {1} and is not recommended for use.", 454 "Oracle", tr("April 2015"))).append("</b><br><br>"); 455 } 456 content.append("<b>") 457 .append(tr("JOSM will soon stop working with this version; we highly recommend you to update to Java {0}.", "8")) 458 .append("</b><br><br>") 459 .append(tr("Would you like to update now ?")); 460 ed.setContent(content.toString()); 461 462 if (ed.showDialog().getValue() == 2) { 463 try { 464 openUrl(url); 465 } catch (IOException e) { 466 Main.warn(e); 467 } 468 } 469 } 470 } 471 }); 472 } 473 474 @Override 475 public boolean setupHttpsCertificate(String entryAlias, KeyStore.TrustedCertificateEntry trustedCert) 476 throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { 477 // TODO setup HTTPS certificate on Unix systems 478 return false; 479 } 480 481 @Override 482 public File getDefaultCacheDirectory() { 483 return new File(Main.pref.getUserDataDirectory(), "cache"); 484 } 485 486 @Override 487 public File getDefaultPrefDirectory() { 488 return new File(System.getProperty("user.home"), ".josm"); 489 } 490 491 @Override 492 public File getDefaultUserDataDirectory() { 493 // Use preferences directory by default 494 return Main.pref.getPreferencesDirectory(); 495 } 496 497 /** 498 * <p>Add more fallback fonts to the Java runtime, in order to get 499 * support for more scripts.</p> 500 * 501 * <p>The font configuration in Java doesn't include some Indic scripts, 502 * even though MS Windows ships with fonts that cover these unicode ranges.</p> 503 * 504 * <p>To fix this, the fontconfig.properties template is copied to the JOSM 505 * cache folder. Then, the additional entries are added to the font 506 * configuration. Finally the system property "sun.awt.fontconfig" is set 507 * to the customized fontconfig.properties file.</p> 508 * 509 * <p>This is a crude hack, but better than no font display at all for these languages. 510 * There is no guarantee, that the template file 511 * ($JAVA_HOME/lib/fontconfig.properties.src) matches the default 512 * configuration (which is in a binary format). 513 * Furthermore, the system property "sun.awt.fontconfig" is undocumented and 514 * may no longer work in future versions of Java.</p> 515 * 516 * <p>Related Java bug: <a href="https://bugs.openjdk.java.net/browse/JDK-8008572">JDK-8008572</a></p> 517 * 518 * @param templateFileName file name of the fontconfig.properties template file 519 */ 520 protected void extendFontconfig(String templateFileName) { 521 String customFontconfigFile = Main.pref.get("fontconfig.properties", null); 522 if (customFontconfigFile != null) { 523 Utils.updateSystemProperty("sun.awt.fontconfig", customFontconfigFile); 524 return; 525 } 526 if (!Main.pref.getBoolean("font.extended-unicode", true)) 527 return; 528 529 String javaLibPath = System.getProperty("java.home") + File.separator + "lib"; 530 Path templateFile = FileSystems.getDefault().getPath(javaLibPath, templateFileName); 531 if (!Files.isReadable(templateFile)) { 532 Main.warn("extended font config - unable to find font config template file "+templateFile.toString()); 533 return; 534 } 535 try (FileInputStream fis = new FileInputStream(templateFile.toFile())) { 536 Properties props = new Properties(); 537 props.load(fis); 538 byte[] content = Files.readAllBytes(templateFile); 539 File cachePath = Main.pref.getCacheDirectory(); 540 Path fontconfigFile = cachePath.toPath().resolve("fontconfig.properties"); 541 OutputStream os = Files.newOutputStream(fontconfigFile); 542 os.write(content); 543 try (Writer w = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8))) { 544 Collection<FontEntry> extrasPref = Main.pref.getListOfStructs( 545 "font.extended-unicode.extra-items", getAdditionalFonts(), FontEntry.class); 546 Collection<FontEntry> extras = new ArrayList<>(); 547 w.append("\n\n# Added by JOSM to extend unicode coverage of Java font support:\n\n"); 548 List<String> allCharSubsets = new ArrayList<>(); 549 for (FontEntry entry: extrasPref) { 550 Collection<String> fontsAvail = getInstalledFonts(); 551 if (fontsAvail != null && fontsAvail.contains(entry.file.toUpperCase(Locale.ENGLISH))) { 552 if (!allCharSubsets.contains(entry.charset)) { 553 allCharSubsets.add(entry.charset); 554 extras.add(entry); 555 } else { 556 Main.trace("extended font config - already registered font for charset ''{0}'' - skipping ''{1}''", 557 entry.charset, entry.name); 558 } 559 } else { 560 Main.trace("extended font config - Font ''{0}'' not found on system - skipping", entry.name); 561 } 562 } 563 for (FontEntry entry: extras) { 564 allCharSubsets.add(entry.charset); 565 if ("".equals(entry.name)) { 566 continue; 567 } 568 String key = "allfonts." + entry.charset; 569 String value = entry.name; 570 String prevValue = props.getProperty(key); 571 if (prevValue != null && !prevValue.equals(value)) { 572 Main.warn("extended font config - overriding ''{0}={1}'' with ''{2}''", key, prevValue, value); 573 } 574 w.append(key + '=' + value + '\n'); 575 } 576 w.append('\n'); 577 for (FontEntry entry: extras) { 578 if ("".equals(entry.name) || "".equals(entry.file)) { 579 continue; 580 } 581 String key = "filename." + entry.name.replace(' ', '_'); 582 String value = entry.file; 583 String prevValue = props.getProperty(key); 584 if (prevValue != null && !prevValue.equals(value)) { 585 Main.warn("extended font config - overriding ''{0}={1}'' with ''{2}''", key, prevValue, value); 586 } 587 w.append(key + '=' + value + '\n'); 588 } 589 w.append('\n'); 590 String fallback = props.getProperty("sequence.fallback"); 591 if (fallback != null) { 592 w.append("sequence.fallback=" + fallback + ',' + Utils.join(",", allCharSubsets) + '\n'); 593 } else { 594 w.append("sequence.fallback=" + Utils.join(",", allCharSubsets) + '\n'); 595 } 596 } 597 Utils.updateSystemProperty("sun.awt.fontconfig", fontconfigFile.toString()); 598 } catch (IOException ex) { 599 Main.error(ex); 600 } 601 } 602 603 /** 604 * Get a list of fonts that are installed on the system. 605 * 606 * Must be done without triggering the Java Font initialization. 607 * (See {@link #extendFontconfig(java.lang.String)}, have to set system 608 * property first, which is then read by sun.awt.FontConfiguration upon initialization.) 609 * 610 * @return list of file names 611 */ 612 public Collection<String> getInstalledFonts() { 613 throw new UnsupportedOperationException(); 614 } 615 616 /** 617 * Get default list of additional fonts to add to the configuration. 618 * 619 * Java will choose thee first font in the list that can render a certain character. 620 * 621 * @return list of FontEntry objects 622 */ 623 public Collection<FontEntry> getAdditionalFonts() { 624 throw new UnsupportedOperationException(); 625 } 626}