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