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