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> &lt; <code>version2</code>,
187    * 0 if <code>version1</code> = <code>version2</code>
188    * and a positive number if <code>version1</code> &gt; <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