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