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.event.KeyEvent; 009import java.io.BufferedReader; 010import java.io.File; 011import java.io.IOException; 012import java.io.InputStreamReader; 013import java.net.URI; 014import java.net.URISyntaxException; 015import java.nio.charset.StandardCharsets; 016import java.nio.file.Files; 017import java.nio.file.Path; 018import java.nio.file.Paths; 019import java.util.Arrays; 020import java.util.Locale; 021 022import javax.swing.JOptionPane; 023 024import org.openstreetmap.josm.Main; 025import org.openstreetmap.josm.gui.ExtendedDialog; 026import org.openstreetmap.josm.gui.util.GuiHelper; 027 028/** 029 * {@code PlatformHook} base implementation. 030 * 031 * Don't write (Main.platform instanceof PlatformHookUnixoid) because other platform 032 * hooks are subclasses of this class. 033 */ 034public class PlatformHookUnixoid implements PlatformHook { 035 036 private String osDescription; 037 038 @Override 039 public void preStartupHook() { 040 // See #12022 - Disable GNOME ATK Java wrapper as it causes a lot of serious trouble 041 if ("org.GNOME.Accessibility.AtkWrapper".equals(System.getProperty("assistive_technologies"))) { 042 System.clearProperty("assistive_technologies"); 043 } 044 } 045 046 @Override 047 public void openUrl(String url) throws IOException { 048 for (String program : Main.pref.getCollection("browser.unix", 049 Arrays.asList("xdg-open", "#DESKTOP#", "$BROWSER", "gnome-open", "kfmclient openURL", "firefox"))) { 050 try { 051 if ("#DESKTOP#".equals(program)) { 052 Desktop.getDesktop().browse(new URI(url)); 053 } else if (program.startsWith("$")) { 054 program = System.getenv().get(program.substring(1)); 055 Runtime.getRuntime().exec(new String[]{program, url}); 056 } else { 057 Runtime.getRuntime().exec(new String[]{program, url}); 058 } 059 return; 060 } catch (IOException | URISyntaxException e) { 061 Main.warn(e); 062 } 063 } 064 } 065 066 @Override 067 public void initSystemShortcuts() { 068 // CHECKSTYLE.OFF: LineLength 069 // TODO: Insert system shortcuts here. See Windows and especially OSX to see how to. 070 for (int i = KeyEvent.VK_F1; i <= KeyEvent.VK_F12; ++i) { 071 Shortcut.registerSystemShortcut("screen:toogle"+i, tr("reserved"), i, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK) 072 .setAutomatic(); 073 } 074 Shortcut.registerSystemShortcut("system:reset", tr("reserved"), KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK) 075 .setAutomatic(); 076 Shortcut.registerSystemShortcut("system:resetX", tr("reserved"), KeyEvent.VK_BACK_SPACE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK) 077 .setAutomatic(); 078 // CHECKSTYLE.ON: LineLength 079 } 080 081 @Override 082 public String getDefaultStyle() { 083 return "javax.swing.plaf.metal.MetalLookAndFeel"; 084 } 085 086 /** 087 * Determines if the distribution is Debian or Ubuntu, or a derivative. 088 * @return {@code true} if the distribution is Debian, Ubuntu or Mint, {@code false} otherwise 089 */ 090 public static boolean isDebianOrUbuntu() { 091 try { 092 String dist = Utils.execOutput(Arrays.asList("lsb_release", "-i", "-s")); 093 return "Debian".equalsIgnoreCase(dist) || "Ubuntu".equalsIgnoreCase(dist) || "Mint".equalsIgnoreCase(dist); 094 } catch (IOException e) { 095 // lsb_release is not available on all Linux systems, so don't log at warning level 096 Main.debug(e); 097 return false; 098 } 099 } 100 101 /** 102 * Determines if the JVM is OpenJDK-based. 103 * @return {@code true} if {@code java.home} contains "openjdk", {@code false} otherwise 104 * @since 6951 105 */ 106 public static boolean isOpenJDK() { 107 String javaHome = System.getProperty("java.home"); 108 return javaHome != null && javaHome.contains("openjdk"); 109 } 110 111 /** 112 * Get the package name including detailed version. 113 * @param packageNames The possible package names (when a package can have different names on different distributions) 114 * @return The package name and package version if it can be identified, null otherwise 115 * @since 7314 116 */ 117 public static String getPackageDetails(String ... packageNames) { 118 try { 119 // CHECKSTYLE.OFF: SingleSpaceSeparator 120 boolean dpkg = Paths.get("/usr/bin/dpkg-query").toFile().exists(); 121 boolean eque = Paths.get("/usr/bin/equery").toFile().exists(); 122 boolean rpm = Paths.get("/bin/rpm").toFile().exists(); 123 // CHECKSTYLE.ON: SingleSpaceSeparator 124 if (dpkg || rpm || eque) { 125 for (String packageName : packageNames) { 126 String[] args; 127 if (dpkg) { 128 args = new String[] {"dpkg-query", "--show", "--showformat", "${Architecture}-${Version}", packageName}; 129 } else if (eque) { 130 args = new String[] {"equery", "-q", "list", "-e", "--format=$fullversion", packageName}; 131 } else { 132 args = new String[] {"rpm", "-q", "--qf", "%{arch}-%{version}", packageName}; 133 } 134 String version = Utils.execOutput(Arrays.asList(args)); 135 if (version != null && !version.contains("not installed")) { 136 return packageName + ':' + version; 137 } 138 } 139 } 140 } catch (IOException e) { 141 Main.warn(e); 142 } 143 return null; 144 } 145 146 /** 147 * Get the Java package name including detailed version. 148 * 149 * Some Java bugs are specific to a certain security update, so in addition 150 * to the Java version, we also need the exact package version. 151 * 152 * @return The package name and package version if it can be identified, null otherwise 153 */ 154 public String getJavaPackageDetails() { 155 String home = System.getProperty("java.home"); 156 if (home.contains("java-8-openjdk") || home.contains("java-1.8.0-openjdk")) { 157 return getPackageDetails("openjdk-8-jre", "java-1_8_0-openjdk", "java-1.8.0-openjdk"); 158 } else if (home.contains("java-9-openjdk") || home.contains("java-1.9.0-openjdk")) { 159 return getPackageDetails("openjdk-9-jre", "java-1_9_0-openjdk", "java-1.9.0-openjdk"); 160 } else if (home.contains("icedtea")) { 161 return getPackageDetails("icedtea-bin"); 162 } else if (home.contains("oracle")) { 163 return getPackageDetails("oracle-jdk-bin", "oracle-jre-bin"); 164 } 165 return null; 166 } 167 168 /** 169 * Get the Web Start package name including detailed version. 170 * 171 * OpenJDK packages are shipped with icedtea-web package, 172 * but its version generally does not match main java package version. 173 * 174 * Simply return {@code null} if there's no separate package for Java WebStart. 175 * 176 * @return The package name and package version if it can be identified, null otherwise 177 */ 178 public String getWebStartPackageDetails() { 179 if (isOpenJDK()) { 180 return getPackageDetails("icedtea-netx", "icedtea-web"); 181 } 182 return null; 183 } 184 185 /** 186 * Get the Gnome ATK wrapper package name including detailed version. 187 * 188 * Debian and Ubuntu derivatives come with a pre-enabled accessibility software 189 * completely buggy that makes Swing crash in a lot of different ways. 190 * 191 * Simply return {@code null} if it's not found. 192 * 193 * @return The package name and package version if it can be identified, null otherwise 194 */ 195 public String getAtkWrapperPackageDetails() { 196 if (isOpenJDK() && isDebianOrUbuntu()) { 197 return getPackageDetails("libatk-wrapper-java"); 198 } 199 return null; 200 } 201 202 protected String buildOSDescription() { 203 String osName = System.getProperty("os.name"); 204 if ("Linux".equalsIgnoreCase(osName)) { 205 try { 206 // Try lsb_release (only available on LSB-compliant Linux systems, 207 // see https://www.linuxbase.org/lsb-cert/productdir.php?by_prod ) 208 Process p = Runtime.getRuntime().exec("lsb_release -ds"); 209 try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { 210 String line = Utils.strip(input.readLine()); 211 if (line != null && !line.isEmpty()) { 212 line = line.replaceAll("\"+", ""); 213 line = line.replaceAll("NAME=", ""); // strange code for some Gentoo's 214 if (line.startsWith("Linux ")) // e.g. Linux Mint 215 return line; 216 else if (!line.isEmpty()) 217 return "Linux " + line; 218 } 219 } 220 } catch (IOException e) { 221 Main.debug(e); 222 // Non LSB-compliant Linux system. List of common fallback release files: http://linuxmafia.com/faq/Admin/release-files.html 223 for (LinuxReleaseInfo info : new LinuxReleaseInfo[]{ 224 new LinuxReleaseInfo("/etc/lsb-release", "DISTRIB_DESCRIPTION", "DISTRIB_ID", "DISTRIB_RELEASE"), 225 new LinuxReleaseInfo("/etc/os-release", "PRETTY_NAME", "NAME", "VERSION"), 226 new LinuxReleaseInfo("/etc/arch-release"), 227 new LinuxReleaseInfo("/etc/debian_version", "Debian GNU/Linux "), 228 new LinuxReleaseInfo("/etc/fedora-release"), 229 new LinuxReleaseInfo("/etc/gentoo-release"), 230 new LinuxReleaseInfo("/etc/redhat-release"), 231 new LinuxReleaseInfo("/etc/SuSE-release") 232 }) { 233 String description = info.extractDescription(); 234 if (description != null && !description.isEmpty()) { 235 return "Linux " + description; 236 } 237 } 238 } 239 } 240 return osName; 241 } 242 243 @Override 244 public String getOSDescription() { 245 if (osDescription == null) { 246 osDescription = buildOSDescription(); 247 } 248 return osDescription; 249 } 250 251 protected static class LinuxReleaseInfo { 252 private final String path; 253 private final String descriptionField; 254 private final String idField; 255 private final String releaseField; 256 private final boolean plainText; 257 private final String prefix; 258 259 public LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField) { 260 this(path, descriptionField, idField, releaseField, false, null); 261 } 262 263 public LinuxReleaseInfo(String path) { 264 this(path, null, null, null, true, null); 265 } 266 267 public LinuxReleaseInfo(String path, String prefix) { 268 this(path, null, null, null, true, prefix); 269 } 270 271 private LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField, boolean plainText, String prefix) { 272 this.path = path; 273 this.descriptionField = descriptionField; 274 this.idField = idField; 275 this.releaseField = releaseField; 276 this.plainText = plainText; 277 this.prefix = prefix; 278 } 279 280 @Override public String toString() { 281 return "ReleaseInfo [path=" + path + ", descriptionField=" + descriptionField + 282 ", idField=" + idField + ", releaseField=" + releaseField + ']'; 283 } 284 285 /** 286 * Extracts OS detailed information from a Linux release file (/etc/xxx-release) 287 * @return The OS detailed information, or {@code null} 288 */ 289 public String extractDescription() { 290 String result = null; 291 if (path != null) { 292 Path p = Paths.get(path); 293 if (p.toFile().exists()) { 294 try (BufferedReader reader = Files.newBufferedReader(p, StandardCharsets.UTF_8)) { 295 String id = null; 296 String release = null; 297 String line; 298 while (result == null && (line = reader.readLine()) != null) { 299 if (line.contains("=")) { 300 String[] tokens = line.split("="); 301 if (tokens.length >= 2) { 302 // Description, if available, contains exactly what we need 303 if (descriptionField != null && descriptionField.equalsIgnoreCase(tokens[0])) { 304 result = Utils.strip(tokens[1]); 305 } else if (idField != null && idField.equalsIgnoreCase(tokens[0])) { 306 id = Utils.strip(tokens[1]); 307 } else if (releaseField != null && releaseField.equalsIgnoreCase(tokens[0])) { 308 release = Utils.strip(tokens[1]); 309 } 310 } 311 } else if (plainText && !line.isEmpty()) { 312 // Files composed of a single line 313 result = Utils.strip(line); 314 } 315 } 316 // If no description has been found, try to rebuild it with "id" + "release" (i.e. "name" + "version") 317 if (result == null && id != null && release != null) { 318 result = id + ' ' + release; 319 } 320 } catch (IOException e) { 321 // Ignore 322 Main.trace(e); 323 } 324 } 325 } 326 // Append prefix if any 327 if (result != null && !result.isEmpty() && prefix != null && !prefix.isEmpty()) { 328 result = prefix + result; 329 } 330 if (result != null) 331 result = result.replaceAll("\"+", ""); 332 return result; 333 } 334 } 335 336 // Method unused, but kept for translation already done. To reuse during Java 9 migration 337 protected void askUpdateJava(final String version, final String url) { 338 GuiHelper.runInEDTAndWait(() -> { 339 ExtendedDialog ed = new ExtendedDialog( 340 Main.parent, 341 tr("Outdated Java version"), 342 new String[]{tr("OK"), tr("Update Java"), tr("Cancel")}); 343 // Check if the dialog has not already been permanently hidden by user 344 if (!ed.toggleEnable("askUpdateJava9").toggleCheckState()) { 345 ed.setButtonIcons(new String[]{"ok", "java", "cancel"}).setCancelButton(3); 346 ed.setMinimumSize(new Dimension(480, 300)); 347 ed.setIcon(JOptionPane.WARNING_MESSAGE); 348 StringBuilder content = new StringBuilder(tr("You are running version {0} of Java.", "<b>"+version+"</b>")) 349 .append("<br><br>"); 350 if ("Sun Microsystems Inc.".equals(System.getProperty("java.vendor")) && !isOpenJDK()) { 351 content.append("<b>").append(tr("This version is no longer supported by {0} since {1} and is not recommended for use.", 352 "Oracle", tr("April 2015"))).append("</b><br><br>"); // TODO: change date once Java 8 EOL is announced 353 } 354 content.append("<b>") 355 .append(tr("JOSM will soon stop working with this version; we highly recommend you to update to Java {0}.", "8")) 356 .append("</b><br><br>") 357 .append(tr("Would you like to update now ?")); 358 ed.setContent(content.toString()); 359 360 if (ed.showDialog().getValue() == 2) { 361 try { 362 openUrl(url); 363 } catch (IOException e) { 364 Main.warn(e); 365 } 366 } 367 } 368 }); 369 } 370 371 /** 372 * Get the dot directory <code>~/.josm</code>. 373 * @return the dot directory 374 */ 375 private static File getDotDirectory() { 376 String dirName = "." + Main.pref.getJOSMDirectoryBaseName().toLowerCase(Locale.ENGLISH); 377 return new File(System.getProperty("user.home"), dirName); 378 } 379 380 /** 381 * Returns true if the dot directory should be used for storing preferences, 382 * cache and user data. 383 * Currently this is the case, if the dot directory already exists. 384 * @return true if the dot directory should be used 385 */ 386 private boolean useDotDirectory() { 387 return getDotDirectory().exists(); 388 } 389 390 @Override 391 public File getDefaultCacheDirectory() { 392 if (useDotDirectory()) { 393 return new File(getDotDirectory(), "cache"); 394 } else { 395 String xdgCacheDir = System.getenv("XDG_CACHE_HOME"); 396 if (xdgCacheDir != null && !xdgCacheDir.isEmpty()) { 397 return new File(xdgCacheDir, Main.pref.getJOSMDirectoryBaseName()); 398 } else { 399 return new File(System.getProperty("user.home") + File.separator + 400 ".cache" + File.separator + Main.pref.getJOSMDirectoryBaseName()); 401 } 402 } 403 } 404 405 @Override 406 public File getDefaultPrefDirectory() { 407 if (useDotDirectory()) { 408 return getDotDirectory(); 409 } else { 410 String xdgConfigDir = System.getenv("XDG_CONFIG_HOME"); 411 if (xdgConfigDir != null && !xdgConfigDir.isEmpty()) { 412 return new File(xdgConfigDir, Main.pref.getJOSMDirectoryBaseName()); 413 } else { 414 return new File(System.getProperty("user.home") + File.separator + 415 ".config" + File.separator + Main.pref.getJOSMDirectoryBaseName()); 416 } 417 } 418 } 419 420 @Override 421 public File getDefaultUserDataDirectory() { 422 if (useDotDirectory()) { 423 return getDotDirectory(); 424 } else { 425 String xdgDataDir = System.getenv("XDG_DATA_HOME"); 426 if (xdgDataDir != null && !xdgDataDir.isEmpty()) { 427 return new File(xdgDataDir, Main.pref.getJOSMDirectoryBaseName()); 428 } else { 429 return new File(System.getProperty("user.home") + File.separator + 430 ".local" + File.separator + "share" + File.separator + Main.pref.getJOSMDirectoryBaseName()); 431 } 432 } 433 } 434 435}