1 /*
   2  * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package java.util.prefs;
  27 
  28 import java.util.HashMap;
  29 import java.util.HashSet;
  30 import java.util.Iterator;
  31 import java.util.Timer;
  32 import java.util.TimerTask;
  33 import java.lang.ref.WeakReference;
  34 
  35 
  36 /*
  37   MacOSXPreferencesFile synchronization:
  38 
  39   Everything is synchronized on MacOSXPreferencesFile.class. This prevents:
  40   * simultaneous updates to cachedFiles or changedFiles
  41   * simultaneous creation of two objects for the same name+user+host triplet
  42   * simultaneous modifications to the same file
  43   * modifications during syncWorld/flushWorld
  44   * (in MacOSXPreferences.removeNodeSpi()) modification or sync during
  45     multi-step node removal process
  46   ... among other things.
  47 */
  48 /*
  49   Timers. There are two timers that control synchronization of prefs data to
  50   and from disk.
  51 
  52   * Sync timer periodically calls syncWorld() to force external disk changes
  53       (e.g. from another VM) into the memory cache. The sync timer runs even
  54       if there are no outstanding local changes. The sync timer syncs all live
  55       MacOSXPreferencesFile objects (the cachedFiles list).
  56     The sync timer period is controlled by the java.util.prefs.syncInterval
  57       property (same as FileSystemPreferences). By default there is *no*
  58       sync timer (unlike FileSystemPreferences); it is only enabled if the
  59       syncInterval property is set. The minimum interval is 5 seconds.
  60 
  61   * Flush timer calls flushWorld() to force local changes to disk.
  62       The flush timer is scheduled to fire some time after each pref change,
  63       unless it's already scheduled to fire before that. syncWorld and
  64       flushWorld will cancel any outstanding flush timer as unnecessary.
  65       The flush timer flushes all changed files (the changedFiles list).
  66     The time between pref write and flush timer call is controlled by the
  67       java.util.prefs.flushDelay property (unlike FileSystemPreferences).
  68       The default is 60 seconds and the minimum is 5 seconds.
  69 
  70   The flush timer's behavior is required by the Java Preferences spec
  71   ("changes will eventually propagate to the persistent backing store with
  72   an implementation-dependent delay"). The sync timer is not required by
  73   the spec (multiple VMs are only required to not corrupt the prefs), but
  74   the periodic sync is implemented by FileSystemPreferences and may be
  75   useful to some programs. The sync timer is disabled by default because
  76   it's expensive and is usually not necessary.
  77 */
  78 
  79 class MacOSXPreferencesFile {
  80 
  81     static {
  82         java.security.AccessController.doPrivileged(new sun.security.action.LoadLibraryAction("osx"));
  83     }
  84 
  85     private class FlushTask extends TimerTask {
  86         public void run() {
  87             MacOSXPreferencesFile.flushWorld();
  88         }
  89     }
  90 
  91     private class SyncTask extends TimerTask {
  92         public void run() {
  93             MacOSXPreferencesFile.syncWorld();
  94         }
  95     }
  96 
  97     // Maps string -> weak reference to MacOSXPreferencesFile
  98     private static HashMap cachedFiles = null;
  99     // Files that may have unflushed changes
 100     private static HashSet changedFiles = null;
 101 
 102 
 103     // Timer and pending sync and flush tasks (which are both scheduled
 104     // on the same timer)
 105     private static Timer timer = null;
 106     private static FlushTask flushTimerTask = null;
 107     private static long flushDelay = -1; // in seconds (min 5, default 60)
 108     private static long syncInterval = -1; // (min 5, default negative == off)
 109 
 110     private String appName;
 111     private long user;
 112     private long host;
 113 
 114     String name() { return appName; }
 115     long user() { return user; }
 116     long host() { return host; }
 117 
 118     // private contructor - use factory method getFile() instead
 119     private MacOSXPreferencesFile(String newName, long newUser, long newHost)
 120     {
 121         appName = newName;
 122         user = newUser;
 123         host = newHost;
 124     }
 125 
 126     // Factory method
 127     // Always returns the same object for the given name+user+host
 128     static synchronized MacOSXPreferencesFile
 129         getFile(String newName, boolean isUser)
 130     {
 131         MacOSXPreferencesFile result = null;
 132 
 133         if (cachedFiles == null) cachedFiles = new HashMap();
 134 
 135         String hashkey =
 136             newName + String.valueOf(isUser);
 137         WeakReference hashvalue = (WeakReference)cachedFiles.get(hashkey);
 138         if (hashvalue != null) {
 139             result = (MacOSXPreferencesFile)hashvalue.get();
 140         }
 141         if (result == null) {
 142             // Java user node == CF current user, any host
 143             // Java system node == CF any user, current host
 144             result = new MacOSXPreferencesFile(newName,
 145                                          isUser ? cfCurrentUser : cfAnyUser,
 146                                          isUser ? cfAnyHost : cfCurrentHost);
 147             cachedFiles.put(hashkey, new WeakReference(result));
 148         }
 149 
 150         // Don't schedule this file for flushing until some nodes or
 151         // keys are added to it.
 152 
 153         // Do set up the sync timer if requested; sync timer affects reads
 154         // as well as writes.
 155         initSyncTimerIfNeeded();
 156 
 157         return result;
 158     }
 159 
 160 
 161     // Write all prefs changes to disk and clear all cached prefs values
 162     // (so the next read will read from disk).
 163     static synchronized boolean syncWorld()
 164     {
 165         boolean ok = true;
 166 
 167         if (cachedFiles != null  &&  !cachedFiles.isEmpty()) {
 168             Iterator iter = cachedFiles.values().iterator();
 169             while (iter.hasNext()) {
 170                 WeakReference ref = (WeakReference)iter.next();
 171                 MacOSXPreferencesFile f = (MacOSXPreferencesFile)ref.get();
 172                 if (f != null) {
 173                     if (!f.synchronize()) ok = false;
 174                 } else {
 175                     iter.remove();
 176                 }
 177             }
 178         }
 179 
 180         // Kill any pending flush
 181         if (flushTimerTask != null) {
 182             flushTimerTask.cancel();
 183             flushTimerTask = null;
 184         }
 185 
 186         // Clear changed file list. The changed files were guaranteed to
 187         // have been in the cached file list (because there was a strong
 188         // reference from changedFiles.
 189         if (changedFiles != null) changedFiles.clear();
 190 
 191         return ok;
 192     }
 193 
 194 
 195     // Sync only current user preferences
 196     static synchronized boolean syncUser() {
 197         boolean ok = true;
 198         if (cachedFiles != null  &&  !cachedFiles.isEmpty()) {
 199             Iterator<WeakReference> iter = cachedFiles.values().iterator();
 200             while (iter.hasNext()) {
 201                 WeakReference ref = iter.next();
 202                 MacOSXPreferencesFile f = (MacOSXPreferencesFile)ref.get();
 203                 if (f != null && f.user == cfCurrentUser) {
 204                         if (!f.synchronize()) ok = false;
 205                 } else {
 206                     iter.remove();
 207                 }
 208             }
 209         }
 210         // Clear changed file list. The changed files were guaranteed to
 211         // have been in the cached file list (because there was a strong
 212         // reference from changedFiles.
 213         if (changedFiles != null) {
 214             Iterator<MacOSXPreferencesFile> iterChanged = changedFiles.iterator();
 215             while (iterChanged.hasNext()) {
 216                 MacOSXPreferencesFile f = iterChanged.next();
 217                 if (f != null && f.user == cfCurrentUser) 
 218                     iterChanged.remove();
 219              }
 220         }
 221         return ok;
 222     }
 223 
 224 
 225 
 226 
 227     // Write all prefs changes to disk, but do not clear all cached prefs
 228     // values. Also kills any scheduled flush task.
 229     // There's no CFPreferencesFlush() (<rdar://problem/3049129>), so lots of cached prefs
 230     // are cleared anyway.
 231     static synchronized boolean flushWorld()
 232     {
 233         boolean ok = true;
 234 
 235         if (changedFiles != null  &&  !changedFiles.isEmpty()) {
 236             Iterator iter = changedFiles.iterator();
 237             while (iter.hasNext()) {
 238                 MacOSXPreferencesFile f = (MacOSXPreferencesFile)iter.next();
 239                 if (!f.synchronize()) ok = false;
 240             }
 241 
 242             changedFiles.clear();
 243         }
 244 
 245         if (flushTimerTask != null) {
 246             flushTimerTask.cancel();
 247             flushTimerTask = null;
 248         }
 249 
 250         return ok;
 251     }
 252 
 253     // Mark this prefs file as changed. The changes will be flushed in
 254     // at most flushDelay() seconds.
 255     // Must be called when synchronized on MacOSXPreferencesFile.class
 256     private void markChanged()
 257     {
 258         // Add this file to the changed file list
 259         if (changedFiles == null) changedFiles = new HashSet();
 260         changedFiles.add(this);
 261 
 262         // Schedule a new flush and a shutdown hook, if necessary
 263         if (flushTimerTask == null) {
 264             flushTimerTask = new FlushTask();
 265             timer().schedule(flushTimerTask, flushDelay() * 1000);
 266         }
 267     }
 268 
 269     // Return the flush delay, initializing from a property if necessary.
 270     private static synchronized long flushDelay()
 271     {
 272         if (flushDelay == -1) {
 273             try {
 274                 // flush delay >= 5, default 60
 275                 flushDelay = Math.max(5, Integer.parseInt(System.getProperty("java.util.prefs.flushDelay", "60")));
 276             } catch (NumberFormatException e) {
 277                 flushDelay = 60;
 278             }
 279         }
 280         return flushDelay;
 281     }
 282 
 283     // Initialize and run the sync timer, if the sync timer property is set
 284     // and the sync timer hasn't already been started.
 285     private static synchronized void initSyncTimerIfNeeded()
 286     {
 287         // syncInterval: -1 is uninitialized, other negative is off,
 288         // positive is seconds between syncs (min 5).
 289 
 290         if (syncInterval == -1) {
 291             try {
 292                 syncInterval = Integer.parseInt(System.getProperty("java.util.prefs.syncInterval", "-2"));
 293                 if (syncInterval >= 0) {
 294                     // minimum of 5 seconds
 295                     syncInterval = Math.max(5, syncInterval);
 296                 } else {
 297                     syncInterval = -2; // default off
 298                 }
 299             } catch (NumberFormatException e) {
 300                 syncInterval = -2; // bad property value - default off
 301             }
 302 
 303             if (syncInterval > 0) {
 304                 timer().schedule(new TimerTask() {
 305                         public void run() { MacOSXPreferencesFile.syncWorld();}
 306                     }, syncInterval * 1000, syncInterval * 1000);
 307             } else {
 308                 // syncInterval property not set. No sync timer ever.
 309             }
 310         }
 311     }
 312 
 313     // Return the timer used for flush and sync, creating it if necessary.
 314     private static synchronized Timer timer()
 315     {
 316         if (timer == null) {
 317             timer = new Timer(true); // daemon
 318             Thread flushThread = new Thread() {
 319                 public void run() {
 320                     flushWorld();
 321                 }
 322             };
 323             /* Set context class loader to null in order to avoid
 324              * keeping a strong reference to an application classloader.
 325              */
 326             flushThread.setContextClassLoader(null);
 327             Runtime.getRuntime().addShutdownHook(flushThread);
 328         }
 329         return timer;
 330     }
 331 
 332 
 333     // Node manipulation
 334     boolean addNode(String path)
 335     {
 336         synchronized(MacOSXPreferencesFile.class) {
 337             markChanged();
 338             return addNode(path, appName, user, host);
 339         }
 340     }
 341 
 342     void removeNode(String path)
 343     {
 344         synchronized(MacOSXPreferencesFile.class) {
 345             markChanged();
 346             removeNode(path, appName, user, host);
 347         }
 348     }
 349 
 350     void addChildToNode(String path, String child)
 351     {
 352         synchronized(MacOSXPreferencesFile.class) {
 353             markChanged();
 354             addChildToNode(path, child+"/", appName, user, host);
 355         }
 356     }
 357 
 358     void removeChildFromNode(String path, String child)
 359     {
 360         synchronized(MacOSXPreferencesFile.class) {
 361             markChanged();
 362             removeChildFromNode(path, child+"/", appName, user, host);
 363         }
 364     }
 365 
 366 
 367     // Key manipulation
 368     void addKeyToNode(String path, String key, String value)
 369     {
 370         synchronized(MacOSXPreferencesFile.class) {
 371             markChanged();
 372             addKeyToNode(path, key, value, appName, user, host);
 373         }
 374     }
 375 
 376     void removeKeyFromNode(String path, String key)
 377     {
 378         synchronized(MacOSXPreferencesFile.class) {
 379             markChanged();
 380             removeKeyFromNode(path, key, appName, user, host);
 381         }
 382     }
 383 
 384     String getKeyFromNode(String path, String key)
 385     {
 386         synchronized(MacOSXPreferencesFile.class) {
 387             return getKeyFromNode(path, key, appName, user, host);
 388         }
 389     }
 390 
 391 
 392     // Enumerators
 393     String[] getChildrenForNode(String path)
 394     {
 395         synchronized(MacOSXPreferencesFile.class) {
 396             return getChildrenForNode(path, appName, user, host);
 397         }
 398     }
 399 
 400     String[] getKeysForNode(String path)
 401     {
 402         synchronized(MacOSXPreferencesFile.class) {
 403             return getKeysForNode(path, appName, user, host);
 404         }
 405     }
 406 
 407 
 408     // Synchronization
 409     boolean synchronize()
 410     {
 411         synchronized(MacOSXPreferencesFile.class) {
 412             return synchronize(appName, user, host);
 413         }
 414     }
 415 
 416 
 417     // CF functions
 418     // Must be called when synchronized on MacOSXPreferencesFile.class
 419     private static final native boolean
 420         addNode(String path, String name, long user, long host);
 421     private static final native void
 422         removeNode(String path, String name, long user, long host);
 423     private static final native void
 424         addChildToNode(String path, String child,
 425                        String name, long user, long host);
 426     private static final native void
 427         removeChildFromNode(String path, String child,
 428                             String name, long user, long host);
 429     private static final native void
 430         addKeyToNode(String path, String key, String value,
 431                      String name, long user, long host);
 432     private static final native void
 433         removeKeyFromNode(String path, String key,
 434                           String name, long user, long host);
 435     private static final native String
 436         getKeyFromNode(String path, String key,
 437                        String name, long user, long host);
 438     private static final native String[]
 439         getChildrenForNode(String path, String name, long user, long host);
 440     private static final native String[]
 441         getKeysForNode(String path, String name, long user, long host);
 442     private static final native boolean
 443         synchronize(String name, long user, long host);
 444 
 445     // CFPreferences host and user values (CFStringRefs)
 446     private static long cfCurrentUser = currentUser();
 447     private static long cfAnyUser = anyUser();
 448     private static long cfCurrentHost = currentHost();
 449     private static long cfAnyHost = anyHost();
 450 
 451     // CFPreferences constant accessors
 452     private static final native long currentUser();
 453     private static final native long anyUser();
 454     private static final native long currentHost();
 455     private static final native long anyHost();
 456 }
 457