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