1 /* 2 * OperatingSystem.java 1 nov. 07 3 * 4 * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com> 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 */ 20 package com.eteks.sweethome3d.tools; 21 22 import java.io.File; 23 import java.io.FileFilter; 24 import java.io.IOException; 25 import java.math.BigInteger; 26 import java.security.AccessControlException; 27 import java.util.ArrayList; 28 import java.util.Comparator; 29 import java.util.List; 30 import java.util.MissingResourceException; 31 import java.util.ResourceBundle; 32 import java.util.Timer; 33 import java.util.TimerTask; 34 import java.util.UUID; 35 36 import com.apple.eio.FileManager; 37 import com.eteks.sweethome3d.model.Home; 38 39 /** 40 * Tools used to test current user operating system. 41 * @author Emmanuel Puybaret 42 */ 43 public class OperatingSystem { 44 private static final String EDITOR_SUB_FOLDER; 45 private static final String APPLICATION_SUB_FOLDER; 46 private static final String TEMPORARY_SUB_FOLDER; 47 private static final String TEMPORARY_SESSION_SUB_FOLDER; 48 49 static { 50 // Retrieve sub folders where is stored application data 51 ResourceBundle resource = ResourceBundle.getBundle(OperatingSystem.class.getName()); 52 if (OperatingSystem.isMacOSX()) { 53 EDITOR_SUB_FOLDER = resource.getString("editorSubFolder.Mac OS X"); 54 APPLICATION_SUB_FOLDER = resource.getString("applicationSubFolder.Mac OS X"); 55 } else if (OperatingSystem.isWindows()) { 56 EDITOR_SUB_FOLDER = resource.getString("editorSubFolder.Windows"); 57 APPLICATION_SUB_FOLDER = resource.getString("applicationSubFolder.Windows"); 58 } else { 59 EDITOR_SUB_FOLDER = resource.getString("editorSubFolder"); 60 APPLICATION_SUB_FOLDER = resource.getString("applicationSubFolder"); 61 } 62 63 String temporarySubFolder; 64 try { 65 temporarySubFolder = resource.getString("temporarySubFolder"); 66 if (temporarySubFolder.trim().length() == 0) { 67 temporarySubFolder = null; 68 } 69 } catch (MissingResourceException ex) { 70 temporarySubFolder = "work"; 71 } 72 try { 73 temporarySubFolder = System.getProperty( 74 "com.eteks.sweethome3d.tools.temporarySubFolder", temporarySubFolder); 75 } catch (AccessControlException ex) { 76 // Don't change temporarySubFolder value 77 } 78 TEMPORARY_SUB_FOLDER = temporarySubFolder; 79 TEMPORARY_SESSION_SUB_FOLDER = UUID.randomUUID().toString(); 80 } 81 82 // This class contains only static methods OperatingSystem()83 private OperatingSystem() { 84 } 85 86 /** 87 * Returns <code>true</code> if current operating is Linux. 88 */ isLinux()89 public static boolean isLinux() { 90 return System.getProperty("os.name").startsWith("Linux"); 91 } 92 93 /** 94 * Returns <code>true</code> if current operating is Windows. 95 */ isWindows()96 public static boolean isWindows() { 97 return System.getProperty("os.name").startsWith("Windows"); 98 } 99 100 /** 101 * Returns <code>true</code> if current operating is Mac OS X. 102 */ isMacOSX()103 public static boolean isMacOSX() { 104 return System.getProperty("os.name").startsWith("Mac OS X"); 105 } 106 107 /** 108 * Returns <code>true</code> if current operating is Mac OS X 10.5 or superior. 109 */ isMacOSXLeopardOrSuperior()110 public static boolean isMacOSXLeopardOrSuperior() { 111 // Just need to test is OS version is different of 10.4 because Sweet Home 3D 112 // isn't supported under Mac OS X versions previous to 10.4 113 return isMacOSX() 114 && !System.getProperty("os.version").startsWith("10.4"); 115 } 116 117 /** 118 * Returns <code>true</code> if current operating is Mac OS X 10.7 or superior. 119 * @since 4.1 120 */ isMacOSXLionOrSuperior()121 public static boolean isMacOSXLionOrSuperior() { 122 return isMacOSX() 123 && compareVersions(System.getProperty("os.version"), "10.7") >= 0; 124 } 125 126 /** 127 * Returns <code>true</code> if current operating is Mac OS X 10.10 or superior. 128 * @since 4.5 129 */ isMacOSXYosemiteOrSuperior()130 public static boolean isMacOSXYosemiteOrSuperior() { 131 return isMacOSX() 132 && compareVersions(System.getProperty("os.version"), "10.10") >= 0; 133 } 134 135 /** 136 * Returns <code>true</code> if current operating is Mac OS X 10.13 or superior. 137 * @since 5.7 138 */ isMacOSXHighSierraOrSuperior()139 public static boolean isMacOSXHighSierraOrSuperior() { 140 return isMacOSX() 141 && compareVersions(System.getProperty("os.version"), "10.13") >= 0; 142 } 143 144 /** 145 * Returns <code>true</code> if current operating is Mac OS X 10.16 or superior. 146 * @since 6.5 147 */ isMacOSXBigSurOrSuperior()148 public static boolean isMacOSXBigSurOrSuperior() { 149 return isMacOSX() 150 && compareVersions(System.getProperty("os.version"), "10.16") >= 0; 151 } 152 /** 153 * Returns <code>true</code> if the given version is greater than or equal to the version 154 * of the current JVM. 155 * @since 4.0 156 */ isJavaVersionGreaterOrEqual(String javaMinimumVersion)157 public static boolean isJavaVersionGreaterOrEqual(String javaMinimumVersion) { 158 return compareVersions(javaMinimumVersion, getComparableJavaVersion()) <= 0; 159 } 160 161 /** 162 * Returns <code>true</code> if the version of the current JVM is greater or equal to the 163 * <code>javaMinimumVersion</code> and smaller than <code>javaMaximumVersion</code>. 164 * @since 4.2 165 */ isJavaVersionBetween(String javaMinimumVersion, String javaMaximumVersion)166 public static boolean isJavaVersionBetween(String javaMinimumVersion, String javaMaximumVersion) { 167 String javaVersion = getComparableJavaVersion(); 168 return compareVersions(javaMinimumVersion, javaVersion) <= 0 169 && compareVersions(javaVersion, javaMaximumVersion) < 0; 170 } 171 getComparableJavaVersion()172 private static String getComparableJavaVersion() { 173 String javaVersion = System.getProperty("java.version"); 174 try { 175 if ("OpenJDK Runtime Environment".equals(System.getProperty("java.runtime.name"))) { 176 // OpenJDK uses a different version system where updates are noted with a -uxx instead of _xx 177 javaVersion = javaVersion.replace("-u", "_"); 178 } 179 } catch (AccessControlException ex) { 180 // Unsigned applet 181 } 182 return javaVersion; 183 } 184 185 /** 186 * Returns a negative number if <code>version1</code> < <code>version2</code>, 187 * 0 if <code>version1</code> = <code>version2</code> 188 * and a positive number if <code>version1</code> > <code>version2</code>. 189 * Version strings are first split into parts, each subpart ending at each punctuation, space 190 * or when a character of a different type is encountered (letter vs digit). Then each numeric 191 * or string subparts are compared to each other, strings being considered greater than null numbers 192 * and pre release strings (i.e. alpha, beta, rc). Examples:<pre> 193 * "" < "1" 194 * "0" < "1.0" 195 * "1.2beta" < "1.2" 196 * "1.2beta" < "1.2beta2" 197 * "1.2beta" < "1.2.0" 198 * "1.2beta4" < "1.2beta10" 199 * "1.2beta4" < "1.2" 200 * "1.2beta4" < "1.2rc" 201 * "1.2alpha" < "1.2beta" 202 * "1.2beta" < "1.2rc" 203 * "1.2rc" < "1.2" 204 * "1.2rc" < "1.2a" 205 * "1.2" < "1.2a" 206 * "1.2.0" < "1.2a" 207 * "1.2a" < "1.2b" 208 * "1.2a" < "1.2.1" 209 * "1.7.0_11" < "1.7.0_12" 210 * "1.7.0_11rc1" < "1.7.0_11rc2" 211 * "1.7.0_11rc" < "1.7.0_11" 212 * "1.7.0_9" < "1.7.0_11rc" 213 * "1.2" < "1.2.1" 214 * "1.2" < "1.2.0.1" 215 * 216 * "1.2" = "1.2.0.0" (missing information is considered as 0) 217 * "1.2beta4" = "1.2 beta-4" (punctuation, space or missing punctuation doesn't influence result) 218 * "1.2beta4" = "1,2,beta,4" 219 * </pre> 220 * @since 4.0 221 */ compareVersions(String version1, String version2)222 public static int compareVersions(String version1, String version2) { 223 List<Object> version1Parts = splitVersion(version1); 224 List<Object> version2Parts = splitVersion(version2); 225 int i = 0; 226 for ( ; i < version1Parts.size() || i < version2Parts.size(); i++) { 227 Object version1Part = i < version1Parts.size() 228 ? convertPreReleaseVersion(version1Parts.get(i)) 229 : BigInteger.ZERO; // Missing part is considered as 0 230 Object version2Part = i < version2Parts.size() 231 ? convertPreReleaseVersion(version2Parts.get(i)) 232 : BigInteger.ZERO; 233 if (version1Part.getClass() == version2Part.getClass()) { 234 @SuppressWarnings({"unchecked", "rawtypes"}) 235 int comparison = ((Comparable)version1Part).compareTo(version2Part); 236 if (comparison != 0) { 237 return comparison; 238 } 239 } else if (version1Part instanceof String) { 240 // An integer subpart < 0 is smaller than a string (except for pre release strings) 241 return ((BigInteger)version2Part).signum() > 0 ? -1 : 1; 242 } else { 243 // A string subpart is greater than an integer >= 0 244 return ((BigInteger)version1Part).signum() > 0 ? 1 : -1; 245 } 246 } 247 return 0; 248 } 249 250 /** 251 * Returns the substrings components of the given <code>version</code>. 252 */ splitVersion(String version)253 private static List<Object> splitVersion(String version) { 254 List<Object> versionParts = new ArrayList<Object>(); 255 StringBuilder subPart = new StringBuilder(); 256 // First split version with punctuation and space 257 for (String part : version.split("\\p{Punct}|\\s")) { 258 for (int i = 0; i < part.length(); ) { 259 subPart.setLength(0); 260 char c = part.charAt(i); 261 if (Character.isDigit(c)) { 262 for ( ; i < part.length() && Character.isDigit(c = part.charAt(i)); i++) { 263 subPart.append(c); 264 } 265 versionParts.add(new BigInteger(subPart.toString())); 266 } else { 267 for ( ; i < part.length() && !Character.isDigit(c = part.charAt(i)); i++) { 268 subPart.append(c); 269 } 270 versionParts.add(subPart.toString()); 271 } 272 } 273 } 274 return versionParts; 275 } 276 277 /** 278 * Returns negative values if the given version part matches a pre release (i.e. alpha, beta, rc) 279 * or returns the parameter itself. 280 */ convertPreReleaseVersion(Object versionPart)281 private static Object convertPreReleaseVersion(Object versionPart) { 282 if (versionPart instanceof String) { 283 String versionPartString = (String)versionPart; 284 if ("alpha".equalsIgnoreCase(versionPartString)) { 285 return new BigInteger("-3"); 286 } else if ("beta".equalsIgnoreCase(versionPartString)) { 287 return new BigInteger("-2"); 288 } else if ("rc".equalsIgnoreCase(versionPartString)) { 289 return new BigInteger("-1"); 290 } 291 } 292 return versionPart; 293 } 294 295 /** 296 * Returns a temporary file that will be deleted when JVM will exit. 297 * @throws IOException if the file couldn't be created 298 */ createTemporaryFile(String prefix, String suffix)299 public static File createTemporaryFile(String prefix, String suffix) throws IOException { 300 File temporaryFolder; 301 try { 302 temporaryFolder = getDefaultTemporaryFolder(true); 303 } catch (IOException ex) { 304 // In case creating default temporary folder failed, use default temporary files folder 305 temporaryFolder = null; 306 } 307 File temporaryFile = File.createTempFile(prefix, suffix, temporaryFolder); 308 temporaryFile.deleteOnExit(); 309 return temporaryFile; 310 } 311 312 /** 313 * Returns a file comparator that sorts file names according to their version number (excluding their extension when they are the same). 314 */ getFileVersionComparator()315 public static Comparator<File> getFileVersionComparator() { 316 return new Comparator<File>() { 317 public int compare(File file1, File file2) { 318 String fileName1 = file1.getName(); 319 String fileName2 = file2.getName(); 320 int extension1Index = fileName1.lastIndexOf('.'); 321 String extension1 = extension1Index != -1 ? fileName1.substring(extension1Index) : null; 322 int extension2Index = fileName2.lastIndexOf('.'); 323 String extension2 = extension2Index != -1 ? fileName2.substring(extension2Index) : null; 324 // If the files have the same extension, remove it 325 if (extension1 != null && extension1.equals(extension2)) { 326 fileName1 = fileName1.substring(0, extension1Index); 327 fileName2 = fileName2.substring(0, extension2Index); 328 } 329 return OperatingSystem.compareVersions(fileName1, fileName2); 330 } 331 }; 332 } 333 334 /** 335 * Deletes all the temporary files created with {@link #createTemporaryFile(String, String) createTemporaryFile}. 336 */ 337 public static void deleteTemporaryFiles() { 338 try { 339 File temporaryFolder = getDefaultTemporaryFolder(false); 340 if (temporaryFolder != null) { 341 for (File temporaryFile : temporaryFolder.listFiles()) { 342 temporaryFile.delete(); 343 } 344 temporaryFolder.delete(); 345 } 346 } catch (IOException ex) { 347 // Ignore temporary folder that can't be found 348 } catch (AccessControlException ex) { 349 } 350 } 351 352 /** 353 * Returns the default folder used to store temporary files created in the program. 354 */ 355 private synchronized static File getDefaultTemporaryFolder(boolean create) throws IOException { 356 if (TEMPORARY_SUB_FOLDER != null) { 357 File temporaryFolder; 358 if (new File(TEMPORARY_SUB_FOLDER).isAbsolute()) { 359 temporaryFolder = new File(TEMPORARY_SUB_FOLDER); 360 } else { 361 temporaryFolder = new File(getDefaultApplicationFolder(), TEMPORARY_SUB_FOLDER); 362 } 363 final String versionPrefix = Home.CURRENT_VERSION + "-"; 364 final File sessionTemporaryFolder = new File(temporaryFolder, 365 versionPrefix + TEMPORARY_SESSION_SUB_FOLDER); 366 if (!sessionTemporaryFolder.exists()) { 367 // Retrieve existing folders working with same Sweet Home 3D version in temporary folder 368 final File [] siblingTemporaryFolders = temporaryFolder.listFiles(new FileFilter() { 369 public boolean accept(File file) { 370 return file.isDirectory() 371 && file.getName().startsWith(versionPrefix); 372 } 373 }); 374 375 // Create temporary folder 376 if (!createTemporaryFolders(sessionTemporaryFolder)) { 377 throw new IOException("Can't create temporary folder " + sessionTemporaryFolder); 378 } 379 380 // Launch a timer that updates modification date of the temporary folder each minute 381 final long updateDelay = 60000; 382 new Timer(true).schedule(new TimerTask() { 383 @Override 384 public void run() { 385 // Ensure modification date is always growing in case system time was adjusted 386 sessionTemporaryFolder.setLastModified(Math.max(System.currentTimeMillis(), 387 sessionTemporaryFolder.lastModified() + updateDelay)); 388 } 389 }, updateDelay, updateDelay); 390 391 if (siblingTemporaryFolders != null 392 && siblingTemporaryFolders.length > 0) { 393 // Launch a timer that will delete in 10 min temporary folders older than a week 394 final long deleteDelay = 10 * 60000; 395 final long age = 7 * 24 * 3600000; 396 new Timer(true).schedule(new TimerTask() { 397 @Override 398 public void run() { 399 long now = System.currentTimeMillis(); 400 for (File siblingTemporaryFolder : siblingTemporaryFolders) { 401 if (siblingTemporaryFolder.exists() 402 && now - siblingTemporaryFolder.lastModified() > age) { 403 delete(siblingTemporaryFolder); 404 } 405 } 406 } 407 408 private void delete(File fileOrFolder) { 409 if (fileOrFolder.isDirectory()) { 410 File [] files = fileOrFolder.listFiles(); 411 for (File file : files) { 412 delete(file); 413 } 414 } 415 fileOrFolder.delete(); 416 } 417 }, deleteDelay); 418 } 419 } 420 return sessionTemporaryFolder; 421 } else { 422 return null; 423 } 424 } 425 426 /** 427 * Creates the temporary folders in parameters and returns <code>true</code> if it was successful. 428 */ 429 private static boolean createTemporaryFolders(File temporaryFolder) { 430 // Inspired from java.io.File#mkdirs 431 if (temporaryFolder.exists()) { 432 return false; 433 } 434 if (temporaryFolder.mkdir()) { 435 temporaryFolder.deleteOnExit(); 436 return true; 437 } 438 File canonicalFile = null; 439 try { 440 canonicalFile = temporaryFolder.getCanonicalFile(); 441 } catch (IOException e) { 442 return false; 443 } 444 File parent = canonicalFile.getParentFile(); 445 if (parent != null 446 && (createTemporaryFolders(parent) || parent.exists()) 447 && canonicalFile.mkdir()) { 448 temporaryFolder.deleteOnExit(); 449 return true; 450 } else { 451 return false; 452 } 453 } 454 455 /** 456 * Returns default application folder. 457 */ 458 public static File getDefaultApplicationFolder() throws IOException { 459 File userApplicationFolder; 460 if (isMacOSX()) { 461 userApplicationFolder = new File(MacOSXFileManager.getApplicationSupportFolder()); 462 } else if (isWindows()) { 463 userApplicationFolder = new File(System.getProperty("user.home"), "Application Data"); 464 // If user Application Data directory doesn't exist, use user home 465 if (!userApplicationFolder.exists()) { 466 userApplicationFolder = new File(System.getProperty("user.home")); 467 } 468 } else { 469 // Unix 470 userApplicationFolder = new File(System.getProperty("user.home")); 471 } 472 return new File(userApplicationFolder, 473 EDITOR_SUB_FOLDER + File.separator + APPLICATION_SUB_FOLDER); 474 } 475 476 /** 477 * File manager class that accesses to Mac OS X specifics. 478 * Do not invoke methods of this class without checking first if 479 * <code>os.name</code> System property is <code>Mac OS X</code>. 480 * This class requires some classes of <code>com.apple.eio</code> package 481 * to compile. 482 */ 483 private static class MacOSXFileManager { 484 public static String getApplicationSupportFolder() throws IOException { 485 // Find application support folder (0x61737570) for user domain (-32763) 486 return FileManager.findFolder((short)-32763, 0x61737570); 487 } 488 } 489 } 490